Upload folder using huggingface_hub
Browse files- .github/workflows/update_space.yml +28 -0
- .gitignore +94 -0
- AUTO_TRADING_GUIDE.md +154 -0
- README.md +211 -8
- data/auto_trading_log_20250401_104519.txt +13 -0
- data/auto_trading_log_20250401_112240.txt +24 -0
- data/cryptic.db +0 -0
- main.py +99 -0
- requirements.txt +54 -0
- results/report_20250328_160002.md +33 -0
- run.sh +34 -0
- run_trader.sh +43 -0
- run_ui.py +39 -0
- run_ui.sh +25 -0
- setup.sh +30 -0
- src/__init__.py +3 -0
- src/crypto_analysis/__init__.py +5 -0
- src/crypto_analysis/config/agents.yaml +48 -0
- src/crypto_analysis/config/tasks.yaml +96 -0
- src/crypto_analysis/crew.py +751 -0
- src/crypto_analysis/main.py +310 -0
- src/crypto_analysis/test_tools.py +147 -0
- src/crypto_analysis/tools/__init__.py +25 -0
- src/crypto_analysis/tools/alpaca_tools.py +179 -0
- src/crypto_analysis/tools/bitcoin_tools.py +398 -0
- src/crypto_analysis/tools/news_tools.py +230 -0
- src/crypto_analysis/tools/order_tools.py +648 -0
- src/crypto_analysis/tools/technical_tools.py +436 -0
- src/crypto_analysis/tools/yahoo_tools.py +483 -0
- src/crypto_analysis/utils/__init__.py +3 -0
- src/crypto_analysis/utils/api_helpers.py +231 -0
- src/stock_analysis/__init__.py +0 -0
- src/stock_analysis/config/agents.yaml +30 -0
- src/stock_analysis/config/tasks.yaml +51 -0
- src/stock_analysis/crew.py +123 -0
- src/stock_analysis/main.py +32 -0
- src/stock_analysis/tools/__init__.py +0 -0
- src/stock_analysis/tools/calculator_tool.py +12 -0
- src/stock_analysis/tools/sec_tools.py +170 -0
- src/ui/__init__.py +8 -0
- src/ui/app.py +1657 -0
- test_crew.py +14 -0
- tests/run_tests.sh +37 -0
- tests/test_all_tools.py +147 -0
- tests/test_bitcoin.py +83 -0
- tests/test_bitcoin_news.py +51 -0
- tests/test_yahoo_tools.py +71 -0
.github/workflows/update_space.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Run Python script
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- huggingface-deployment
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
build:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout
|
| 14 |
+
uses: actions/checkout@v2
|
| 15 |
+
|
| 16 |
+
- name: Set up Python
|
| 17 |
+
uses: actions/setup-python@v2
|
| 18 |
+
with:
|
| 19 |
+
python-version: '3.9'
|
| 20 |
+
|
| 21 |
+
- name: Install Gradio
|
| 22 |
+
run: python -m pip install gradio
|
| 23 |
+
|
| 24 |
+
- name: Log in to Hugging Face
|
| 25 |
+
run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")'
|
| 26 |
+
|
| 27 |
+
- name: Deploy to Spaces
|
| 28 |
+
run: gradio deploy
|
.gitignore
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual Environment
|
| 2 |
+
venv/
|
| 3 |
+
env/
|
| 4 |
+
ENV/
|
| 5 |
+
|
| 6 |
+
# Python bytecode
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
|
| 11 |
+
# Distribution / packaging
|
| 12 |
+
dist/
|
| 13 |
+
build/
|
| 14 |
+
*.egg-info/
|
| 15 |
+
*.egg
|
| 16 |
+
|
| 17 |
+
# Environment variables
|
| 18 |
+
.env
|
| 19 |
+
.env.local
|
| 20 |
+
.env.development.local
|
| 21 |
+
.env.test.local
|
| 22 |
+
.env.production.local
|
| 23 |
+
# Allow .env.example
|
| 24 |
+
!.env.example
|
| 25 |
+
|
| 26 |
+
# Jupyter Notebook
|
| 27 |
+
.ipynb_checkpoints
|
| 28 |
+
|
| 29 |
+
# IDE files
|
| 30 |
+
.idea/
|
| 31 |
+
.vscode/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
.cursor/
|
| 35 |
+
|
| 36 |
+
# Unit test / coverage reports
|
| 37 |
+
htmlcov/
|
| 38 |
+
.tox/
|
| 39 |
+
.coverage
|
| 40 |
+
.coverage.*
|
| 41 |
+
.cache
|
| 42 |
+
nosetests.xml
|
| 43 |
+
coverage.xml
|
| 44 |
+
*.cover
|
| 45 |
+
*.py,cover
|
| 46 |
+
.hypothesis/
|
| 47 |
+
.pytest_cache/
|
| 48 |
+
|
| 49 |
+
# Result files and data
|
| 50 |
+
results/*.json
|
| 51 |
+
results/*.txt
|
| 52 |
+
db/*.db
|
| 53 |
+
*.sqlite
|
| 54 |
+
*.sqlite3
|
| 55 |
+
|
| 56 |
+
# Log files
|
| 57 |
+
logs/
|
| 58 |
+
*.log
|
| 59 |
+
|
| 60 |
+
# Other
|
| 61 |
+
.DS_Store
|
| 62 |
+
Thumbs.db
|
| 63 |
+
node_modules/
|
| 64 |
+
|
| 65 |
+
# Project specific
|
| 66 |
+
requirements_installed
|
| 67 |
+
*__pycache__/
|
| 68 |
+
**/__pycache__/
|
| 69 |
+
**/._*
|
| 70 |
+
|
| 71 |
+
# Large files
|
| 72 |
+
*.pkl
|
| 73 |
+
*.parquet
|
| 74 |
+
*.model
|
| 75 |
+
*.bin
|
| 76 |
+
*.h5
|
| 77 |
+
*.hdf5
|
| 78 |
+
*.pickle
|
| 79 |
+
*.joblib
|
| 80 |
+
*.tar.gz
|
| 81 |
+
*.zip
|
| 82 |
+
*.rar
|
| 83 |
+
*.7z
|
| 84 |
+
|
| 85 |
+
# Database files (extended)
|
| 86 |
+
db/chroma.sqlite3
|
| 87 |
+
db/chroma/*
|
| 88 |
+
**/*.db-shm
|
| 89 |
+
**/*.db-wal
|
| 90 |
+
|
| 91 |
+
# Cache directories
|
| 92 |
+
**/.cache/
|
| 93 |
+
**/cache/
|
| 94 |
+
**/__pycache__/
|
AUTO_TRADING_GUIDE.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Automated Trading Guide for CrypticAI
|
| 2 |
+
|
| 3 |
+
This guide provides detailed instructions for setting up and running the CrypticAI trading system for extended periods of time (days or weeks).
|
| 4 |
+
|
| 5 |
+
## Getting Started
|
| 6 |
+
|
| 7 |
+
### Prerequisites
|
| 8 |
+
|
| 9 |
+
- Python 3.9 or higher
|
| 10 |
+
- All dependencies installed (see `requirements.txt`)
|
| 11 |
+
- Alpaca API keys properly configured in your `.env` file
|
| 12 |
+
- Enough trading capital in your Alpaca account
|
| 13 |
+
|
| 14 |
+
### Basic Setup
|
| 15 |
+
|
| 16 |
+
1. Configure your API keys in `.env` file:
|
| 17 |
+
```
|
| 18 |
+
ALPACA_API_KEY=your_api_key_here
|
| 19 |
+
ALPACA_API_SECRET=your_api_secret_here
|
| 20 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
2. Make the restart script executable:
|
| 24 |
+
```bash
|
| 25 |
+
chmod +x run_trader.sh
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Running Automated Trading
|
| 29 |
+
|
| 30 |
+
### Option 1: Using the Web Interface
|
| 31 |
+
|
| 32 |
+
1. Start the application:
|
| 33 |
+
```bash
|
| 34 |
+
python src/ui/app.py
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
2. Open the web interface at http://localhost:7860
|
| 38 |
+
|
| 39 |
+
3. Navigate to the "Strategy Configuration" tab and either:
|
| 40 |
+
- Load an existing strategy
|
| 41 |
+
- Create a new strategy
|
| 42 |
+
|
| 43 |
+
4. Set your desired timeframe (how often analysis runs) and maximum allocation percentage
|
| 44 |
+
|
| 45 |
+
5. Go to the "Automated Analysis" tab and click "Start Automated Analysis"
|
| 46 |
+
|
| 47 |
+
6. The system will now run continuously until stopped, executing trades based on your strategy
|
| 48 |
+
|
| 49 |
+
### Option 2: Using the Monitor Script (Recommended for Long-Term)
|
| 50 |
+
|
| 51 |
+
For running the system for days or weeks, use the restart script:
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
./run_trader.sh
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
This script will:
|
| 58 |
+
- Start the CrypticAI application
|
| 59 |
+
- Automatically restart it if it crashes
|
| 60 |
+
- Log all restarts to `trader_monitor.log`
|
| 61 |
+
|
| 62 |
+
### Using Screen or Tmux for Remote Servers
|
| 63 |
+
|
| 64 |
+
If running on a remote server, use `screen` or `tmux` to keep the session alive:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Start a new screen session
|
| 68 |
+
screen -S cryptic_trading
|
| 69 |
+
|
| 70 |
+
# Then run the monitor script
|
| 71 |
+
./run_trader.sh
|
| 72 |
+
|
| 73 |
+
# Detach from the session with Ctrl+A, D
|
| 74 |
+
# You can now safely close your SSH connection
|
| 75 |
+
|
| 76 |
+
# To reconnect later:
|
| 77 |
+
screen -r cryptic_trading
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Monitoring Your Trades
|
| 81 |
+
|
| 82 |
+
### Logs
|
| 83 |
+
|
| 84 |
+
The system creates detailed logs of all trading activity:
|
| 85 |
+
|
| 86 |
+
1. **Trading Session Logs**: Located in `data/` directory with names like `auto_trading_log_20250401_123045.txt`
|
| 87 |
+
- Contains all analysis results, trades, positions, and account balances
|
| 88 |
+
- A new log file is created each time the automated analysis is started
|
| 89 |
+
|
| 90 |
+
2. **Monitor Logs**: If using the restart script, check `trader_monitor.log` for application crashes and restarts
|
| 91 |
+
|
| 92 |
+
### Web Interface
|
| 93 |
+
|
| 94 |
+
While the system is running, you can monitor activity through the web interface:
|
| 95 |
+
|
| 96 |
+
1. "Monitoring" tab:
|
| 97 |
+
- Shows recent analysis results
|
| 98 |
+
- Current positions
|
| 99 |
+
- Transaction history
|
| 100 |
+
|
| 101 |
+
2. "Trading" tab:
|
| 102 |
+
- Displays account summary
|
| 103 |
+
- Current active positions
|
| 104 |
+
|
| 105 |
+
## Analyzing Trading Performance
|
| 106 |
+
|
| 107 |
+
After running the system for a period, analyze performance:
|
| 108 |
+
|
| 109 |
+
1. Review the trading logs in `data/` directory
|
| 110 |
+
2. Check account performance in "Trading" tab
|
| 111 |
+
3. Review all transactions in "Monitoring" tab
|
| 112 |
+
|
| 113 |
+
## Stopping the System
|
| 114 |
+
|
| 115 |
+
To stop automated trading:
|
| 116 |
+
|
| 117 |
+
1. From the web interface:
|
| 118 |
+
- Go to "Automated Analysis" tab
|
| 119 |
+
- Click "Stop Automated Analysis"
|
| 120 |
+
|
| 121 |
+
2. If using the monitor script:
|
| 122 |
+
- Press Ctrl+C to terminate the script
|
| 123 |
+
- Or kill the screen session with `screen -X -S cryptic_trading quit`
|
| 124 |
+
|
| 125 |
+
## Troubleshooting
|
| 126 |
+
|
| 127 |
+
### Common Issues
|
| 128 |
+
|
| 129 |
+
1. **API Rate Limits**: If you encounter rate limit errors, consider increasing the timeframe interval
|
| 130 |
+
|
| 131 |
+
2. **Application Crashes**: Check `trader_monitor.log` for error details if using the restart script
|
| 132 |
+
|
| 133 |
+
3. **Connection Issues**: If remote connection drops, the script will continue running in screen/tmux
|
| 134 |
+
|
| 135 |
+
### Emergency Stop
|
| 136 |
+
|
| 137 |
+
If you need to quickly stop all trading activity:
|
| 138 |
+
|
| 139 |
+
1. Go to the "Trading" tab
|
| 140 |
+
2. Click "Reset Portfolio" to close all positions
|
| 141 |
+
|
| 142 |
+
## Best Practices
|
| 143 |
+
|
| 144 |
+
1. **Start Small**: Begin with small allocation percentages to test your strategy
|
| 145 |
+
|
| 146 |
+
2. **Monitor Regularly**: Check on the system daily to ensure it's functioning correctly
|
| 147 |
+
|
| 148 |
+
3. **Backup Logs**: Periodically backup your logs for analysis
|
| 149 |
+
|
| 150 |
+
4. **Update Strategies**: Refine your strategy based on performance data from logs
|
| 151 |
+
|
| 152 |
+
5. **Server Uptime**: If running on your own server, ensure it has reliable power and internet connectivity
|
| 153 |
+
|
| 154 |
+
Happy automated trading with CrypticAI!
|
README.md
CHANGED
|
@@ -1,12 +1,215 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
colorFrom: red
|
| 5 |
-
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.23.
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
---
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: cryptic
|
| 3 |
+
app_file: src/ui/app.py
|
|
|
|
|
|
|
| 4 |
sdk: gradio
|
| 5 |
+
sdk_version: 5.23.1
|
|
|
|
|
|
|
| 6 |
---
|
| 7 |
+
# CrypticAI - Bitcoin Sentiment Analysis
|
| 8 |
|
| 9 |
+
An AI agentic workflow for Bitcoin price sentiment analysis that combines technical analysis strategies, news sentiment, and market context to provide trading recommendations with confidence scores.
|
| 10 |
+
|
| 11 |
+
## Overview
|
| 12 |
+
|
| 13 |
+
CrypticAI uses a four-stage agentic workflow to analyze Bitcoin's market conditions:
|
| 14 |
+
|
| 15 |
+
1. **Technical Strategy Agent** - Implements a 5-minute timeframe RSI + Bollinger Bands trading strategy to generate precise buy/sell signals
|
| 16 |
+
2. **Initial Analysis Agent** - Examines the technical strategy signal alongside broader market indicators, news sentiment, and cryptocurrency market context
|
| 17 |
+
3. **Reflection Agent** - Critically evaluates both the technical strategy and initial analysis to identify potential blind spots
|
| 18 |
+
4. **Final Synthesis Agent** - Produces a final recommendation with confidence score and portfolio allocation percentage, and executes trades via Alpaca
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- Technical trading strategy using RSI and Bollinger Bands indicators
|
| 23 |
+
- Broader technical analysis using RSI, ADX, and Bollinger Bands across multiple timeframes
|
| 24 |
+
- News and social media sentiment analysis of Bitcoin and crypto markets
|
| 25 |
+
- Comprehensive cryptocurrency market context analysis including:
|
| 26 |
+
- BTC dominance metrics
|
| 27 |
+
- Market-wide trends across top cryptocurrencies
|
| 28 |
+
- Comparative performance data between Bitcoin and other major coins
|
| 29 |
+
- Multiple data sources including Alpaca and Yahoo Finance APIs
|
| 30 |
+
- Structured trading recommendations with confidence scores and portfolio allocation percentages
|
| 31 |
+
- Automatic trade execution through the Alpaca Crypto API
|
| 32 |
+
- Option to run in monitoring mode for continuous analysis
|
| 33 |
+
- Interactive Gradio UI for strategy tuning and order monitoring
|
| 34 |
+
- **Enhanced error handling with exponential backoff retry mechanism**
|
| 35 |
+
- **Data validation and graceful degradation when data sources are unavailable**
|
| 36 |
+
- **Comprehensive logging for improved traceability**
|
| 37 |
+
- **Intelligent caching system to reduce API calls and handle temporary outages**
|
| 38 |
+
|
| 39 |
+
## Setup
|
| 40 |
+
|
| 41 |
+
### Prerequisites
|
| 42 |
+
|
| 43 |
+
- Python 3.10 or higher
|
| 44 |
+
- API keys for:
|
| 45 |
+
- Alpaca (for Bitcoin price data and trade execution)
|
| 46 |
+
- OpenAI (for the LLM agents)
|
| 47 |
+
|
| 48 |
+
### Installation
|
| 49 |
+
|
| 50 |
+
1. Clone the repository:
|
| 51 |
+
```
|
| 52 |
+
git clone https://github.com/yourusername/crypticai.git
|
| 53 |
+
cd crypticai
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
2. Create and activate a virtual environment:
|
| 57 |
+
```
|
| 58 |
+
python -m venv venv
|
| 59 |
+
source venv/bin/activate # On Windows, use: venv\Scripts\activate
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
3. Install dependencies:
|
| 63 |
+
```
|
| 64 |
+
pip install -r requirements.txt
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
4. Create a `.env` file with your API keys:
|
| 68 |
+
```
|
| 69 |
+
ALPACA_API_KEY=your_alpaca_api_key
|
| 70 |
+
ALPACA_API_SECRET=your_alpaca_api_secret
|
| 71 |
+
NEWS_API_KEY=your_newsapi_key
|
| 72 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Usage
|
| 76 |
+
|
| 77 |
+
### Single Analysis
|
| 78 |
+
|
| 79 |
+
To run a one-time analysis:
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
python main.py
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Monitoring Mode
|
| 86 |
+
|
| 87 |
+
To run in monitoring mode (analysis every 4 hours):
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
python -m src.crypto_analysis.main monitor
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Press `Ctrl+C` to exit monitoring mode.
|
| 94 |
+
|
| 95 |
+
### Trading Dashboard UI
|
| 96 |
+
|
| 97 |
+
To launch the interactive trading dashboard UI:
|
| 98 |
+
|
| 99 |
+
```
|
| 100 |
+
python run_ui.py
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
The UI provides:
|
| 104 |
+
|
| 105 |
+
1. **Strategy Configuration** - Adjust RSI thresholds and Bollinger Bands parameters
|
| 106 |
+
2. **Trading** - Execute manual trades and view account information
|
| 107 |
+
3. **Monitoring** - Track active positions and order history
|
| 108 |
+
4. **Automated Analysis** - Run the strategy in the background (every 5 minutes)
|
| 109 |
+
|
| 110 |
+
### Training Mode
|
| 111 |
+
|
| 112 |
+
To train the crew (experimental feature for improved performance):
|
| 113 |
+
|
| 114 |
+
```
|
| 115 |
+
python -m src.crypto_analysis.main train 5 # Run 5 training iterations
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
## Output
|
| 119 |
+
|
| 120 |
+
The analysis produces a trading recommendation with:
|
| 121 |
+
|
| 122 |
+
- **Signal**: BUY, SELL, or HOLD
|
| 123 |
+
- **Confidence**: Confidence score as percentage (0-100%)
|
| 124 |
+
- **Allocation**: Suggested portfolio allocation percentage
|
| 125 |
+
- **Reasoning**: Concise explanation for the recommendation
|
| 126 |
+
- **Tool Error Assessment**: Information about any data retrieval issues
|
| 127 |
+
- **Data Reliability**: Assessment of the reliability of the data used
|
| 128 |
+
|
| 129 |
+
Results are displayed in the console and saved as JSON files in the `results` directory. Detailed logs are also saved to the `logs` directory.
|
| 130 |
+
|
| 131 |
+
## Technical Strategies
|
| 132 |
+
|
| 133 |
+
### 5-Minute RSI + Bollinger Bands Strategy
|
| 134 |
+
|
| 135 |
+
The technical analysis agent implements a structured 5-minute timeframe strategy:
|
| 136 |
+
|
| 137 |
+
#### Buy Signal (Long)
|
| 138 |
+
1. RSI falls below 40 (mildly oversold condition)
|
| 139 |
+
2. Price touches or approaches the lower Bollinger Band
|
| 140 |
+
3. Optimal entry when price starts bouncing up from the lower band
|
| 141 |
+
|
| 142 |
+
#### Sell Signal (Short)
|
| 143 |
+
1. RSI rises above 60 (mildly overbought condition)
|
| 144 |
+
2. Price touches or approaches the upper Bollinger Band
|
| 145 |
+
3. Optimal entry when price starts reversing down from the upper band
|
| 146 |
+
|
| 147 |
+
### Broader Technical Analysis
|
| 148 |
+
|
| 149 |
+
The system also evaluates:
|
| 150 |
+
|
| 151 |
+
1. Market ranging/trending conditions using ADX
|
| 152 |
+
2. RSI values across multiple timeframes
|
| 153 |
+
3. Price positions relative to Bollinger Bands
|
| 154 |
+
4. Support/resistance levels and trend lines
|
| 155 |
+
|
| 156 |
+
### Market Context Analysis
|
| 157 |
+
|
| 158 |
+
The YahooCryptoMarketTool enhances analysis by providing:
|
| 159 |
+
|
| 160 |
+
1. Market-wide cryptocurrency trends (bullish/bearish)
|
| 161 |
+
2. Bitcoin dominance percentage of total market cap
|
| 162 |
+
3. Comparative performance data for top 10 cryptocurrencies
|
| 163 |
+
4. Day-over-day and week-over-week price changes across the market
|
| 164 |
+
|
| 165 |
+
## Data Tools
|
| 166 |
+
|
| 167 |
+
The system leverages several specialized data tools:
|
| 168 |
+
|
| 169 |
+
1. **YahooBitcoinDataTool** - Fetches Bitcoin price data from Yahoo Finance
|
| 170 |
+
2. **YahooCryptoMarketTool** - Provides broader cryptocurrency market context
|
| 171 |
+
3. **RealBitcoinNewsTool** - Gathers latest Bitcoin news and sentiment
|
| 172 |
+
4. **TechnicalAnalysisStrategy** - Applies technical indicators to generate signals
|
| 173 |
+
5. **AlpacaCryptoOrderTool** - Executes trade orders based on final recommendations
|
| 174 |
+
|
| 175 |
+
## Reliability Features
|
| 176 |
+
|
| 177 |
+
### Error Handling and Retry Logic
|
| 178 |
+
|
| 179 |
+
All API calls include robust error handling with configurable retry mechanisms:
|
| 180 |
+
|
| 181 |
+
1. **Exponential Backoff** - Automatic retry with increasing wait times between attempts
|
| 182 |
+
2. **Alternative Data Sources** - Fallback to alternative ticker symbols when primary ones fail
|
| 183 |
+
3. **Data Validation** - Verification of data completeness and format before proceeding
|
| 184 |
+
4. **Graceful Degradation** - The system continues to function with reduced confidence when some data sources are unavailable
|
| 185 |
+
|
| 186 |
+
### Caching System
|
| 187 |
+
|
| 188 |
+
An intelligent caching system reduces API calls and improves reliability:
|
| 189 |
+
|
| 190 |
+
1. **Time-Based Caching** - Results are cached for configurable time periods
|
| 191 |
+
2. **Cache Validation** - Automatic detection of when cached data is stale
|
| 192 |
+
3. **Memory-Efficient Storage** - Optimized storage of cached data to prevent memory issues
|
| 193 |
+
|
| 194 |
+
### Comprehensive Logging
|
| 195 |
+
|
| 196 |
+
Enhanced logging provides traceability and debugging capabilities:
|
| 197 |
+
|
| 198 |
+
1. **Structured Logs** - All system activities are logged with timestamps and contextual information
|
| 199 |
+
2. **Log Rotation** - Daily log files to prevent excessive file sizes
|
| 200 |
+
3. **Error Tracebacks** - Full error traces captured for debugging
|
| 201 |
+
4. **Status Monitoring** - Tool state tracking throughout the analysis process
|
| 202 |
+
|
| 203 |
+
## Extending the System
|
| 204 |
+
|
| 205 |
+
The system is designed to be modular and extensible:
|
| 206 |
+
|
| 207 |
+
1. Add new tools in the `src/crypto_analysis/tools` directory
|
| 208 |
+
2. Modify agent configurations in `src/crypto_analysis/config/agents.yaml`
|
| 209 |
+
3. Adjust task descriptions in `src/crypto_analysis/config/tasks.yaml`
|
| 210 |
+
4. Add new technical strategies in `src/crypto_analysis/tools/technical_tools.py`
|
| 211 |
+
5. Customize error handling in the `src/crypto_analysis/utils/api_helpers.py` file
|
| 212 |
+
|
| 213 |
+
## License
|
| 214 |
+
|
| 215 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
data/auto_trading_log_20250401_104519.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Automated trading session started at 2025-04-01T10:45:19.642169
|
| 2 |
+
Strategy parameters: {"timeframe_minutes": 60, "max_allocation_percentage": 5, "strategy_text": "Buy when RSI is below 20 and the price is near the lower Bollinger Band (position < 0.2).\nSell when RSI is above 90 and the price is near the upper Bollinger Band (position > 0.8).\nIncrease confidence if the ADX shows a strong trend (> 25) in the same direction.\nUse 40% of available capital for trades if confidence is high (> 70), otherwise use 20%."}
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
--- Analysis run at 2025-04-01T10:45:19.642900 ---
|
| 6 |
+
Signal: hold, Confidence: 50%, Allocation: 0%
|
| 7 |
+
Reasoning: A hold position is prudent given stable technical signals, bullish broader market, and optimistic sentiment. No immediate allocation change is warranted. Monitoring for developments is advised.
|
| 8 |
+
Order info: Account check completed
|
| 9 |
+
|
| 10 |
+
Current Positions:
|
| 11 |
+
BTCUSD: 0.326226192 @ $85148.722929033 - P/L: $-678.92 (-2.44%)
|
| 12 |
+
|
| 13 |
+
Account Balance: $63823.62, Equity: $97356.49
|
data/auto_trading_log_20250401_112240.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Automated trading session started at 2025-04-01T11:22:40.319064
|
| 2 |
+
Strategy parameters: {"timeframe_minutes": 60, "max_allocation_percentage": 5, "strategy_text": "Buy when RSI is below 20 and the price is near the lower Bollinger Band (position < 0.2).\nSell when RSI is above 90 and the price is near the upper Bollinger Band (position > 0.8).\nIncrease confidence if the ADX shows a strong trend (> 25) in the same direction.\nUse 40% of available capital for trades if confidence is high (> 70), otherwise use 20%."}
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
--- Analysis run at 2025-04-01T11:22:40.319524 ---
|
| 6 |
+
Signal: buy, Confidence: 30%, Allocation: 5%
|
| 7 |
+
Reasoning: **
|
| 8 |
+
- The technical indicators are neutral, with low confidence levels, leading to a hold recommendation. The broader market context is bullish, but Bitcoin's recent bearish trend and mixed sentiment suggest caution. No clear buy or sell signals warrant a change in the current position, thus recommending to hold with no allocation.
|
| 9 |
+
Order info: Account check completed
|
| 10 |
+
|
| 11 |
+
Current Positions:
|
| 12 |
+
BTCUSD: 0.326226192 @ $85148.722929033 - P/L: $-598.67 (-2.16%)
|
| 13 |
+
|
| 14 |
+
Account Balance: $63823.62, Equity: $97436.75
|
| 15 |
+
|
| 16 |
+
--- Analysis run at 2025-04-01T12:24:04.632925 ---
|
| 17 |
+
Signal: sell, Confidence: 60%, Allocation: 20%
|
| 18 |
+
Reasoning: The technical indicators, particularly the Bollinger Bands, indicate a sell condition with moderate confidence supported by the ADX. However, the broader market's bullish trend and positive sentiment suggest caution. I have adjusted the confidence level to 60% to reflect these mixed signals. Given the allocation recommendation of 20%, this trade remains within the user's maximum limit.
|
| 19 |
+
Order info: manually based on this recommendation and detailed analysis.
|
| 20 |
+
|
| 21 |
+
Current Positions:
|
| 22 |
+
BTCUSD: 0.260980952 @ $85148.722929033 - P/L: $-481.7 (-2.17%)
|
| 23 |
+
|
| 24 |
+
Account Balance: $69254.43, Equity: $97428.98
|
data/cryptic.db
ADDED
|
Binary file (41 kB). View file
|
|
|
main.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Bitcoin Analysis Project - Main Script
|
| 4 |
+
|
| 5 |
+
This script runs the Bitcoin analysis crew to generate investment recommendations
|
| 6 |
+
integrating technical analysis, news sentiment, and market context.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
# Load environment variables
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# Check for required API keys
|
| 19 |
+
if not os.getenv("OPENAI_API_KEY"):
|
| 20 |
+
print("Error: OPENAI_API_KEY not found in environment variables")
|
| 21 |
+
print("Please set up your .env file with the required API keys")
|
| 22 |
+
exit(1)
|
| 23 |
+
|
| 24 |
+
if not os.getenv("ALPACA_API_KEY") or not os.getenv("ALPACA_API_SECRET"):
|
| 25 |
+
print("Error: ALPACA_API_KEY or ALPACA_API_SECRET not found in environment variables")
|
| 26 |
+
print("Please set up your .env file with the required Alpaca API keys")
|
| 27 |
+
exit(1)
|
| 28 |
+
|
| 29 |
+
# Import the BitcoinAnalysisCrew
|
| 30 |
+
from src.crypto_analysis.crew import BitcoinAnalysisCrew
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
"""Run the Bitcoin analysis project"""
|
| 34 |
+
print("=" * 80)
|
| 35 |
+
print("BITCOIN ANALYSIS PROJECT")
|
| 36 |
+
print("=" * 80)
|
| 37 |
+
print(f"Run started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 38 |
+
print("Initializing Bitcoin Analysis Crew...")
|
| 39 |
+
|
| 40 |
+
# Create the crew
|
| 41 |
+
try:
|
| 42 |
+
crew = BitcoinAnalysisCrew()
|
| 43 |
+
print("Crew initialized successfully")
|
| 44 |
+
print("Starting Bitcoin analysis...")
|
| 45 |
+
|
| 46 |
+
# Run the analysis
|
| 47 |
+
result = crew.run_analysis()
|
| 48 |
+
|
| 49 |
+
# Print the results
|
| 50 |
+
print("\n" + "=" * 40)
|
| 51 |
+
print("ANALYSIS RESULTS")
|
| 52 |
+
print("=" * 40)
|
| 53 |
+
print(f"Signal: {result.get('signal', 'unknown').upper()}")
|
| 54 |
+
print(f"Confidence: {result.get('confidence', 0)}%")
|
| 55 |
+
print(f"Allocation: {result.get('allocation_percentage', 0)}%")
|
| 56 |
+
if 'reasoning' in result:
|
| 57 |
+
print("\nReasoning:")
|
| 58 |
+
print(result['reasoning'])
|
| 59 |
+
|
| 60 |
+
# Save the results to a file
|
| 61 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 62 |
+
results_dir = "results"
|
| 63 |
+
os.makedirs(results_dir, exist_ok=True)
|
| 64 |
+
|
| 65 |
+
results_file = os.path.join(results_dir, f"bitcoin_analysis_{timestamp}.json")
|
| 66 |
+
with open(results_file, "w") as f:
|
| 67 |
+
json.dump(result, f, indent=2)
|
| 68 |
+
|
| 69 |
+
print(f"\nResults saved to {results_file}")
|
| 70 |
+
print("=" * 80)
|
| 71 |
+
|
| 72 |
+
# Check if order execution happened
|
| 73 |
+
if 'order_execution' in result:
|
| 74 |
+
print("\nORDER EXECUTION RESULTS:")
|
| 75 |
+
print("-" * 40)
|
| 76 |
+
order_result = result['order_execution']
|
| 77 |
+
if isinstance(order_result, dict):
|
| 78 |
+
if order_result.get('success', False):
|
| 79 |
+
print(f"Order {order_result.get('order_id', 'unknown')} executed successfully!")
|
| 80 |
+
print(f"Side: {order_result.get('side', 'unknown').upper()}")
|
| 81 |
+
print(f"Symbol: {order_result.get('symbol', 'unknown')}")
|
| 82 |
+
print(f"Quantity: {order_result.get('quantity', 'unknown')}")
|
| 83 |
+
print(f"Status: {order_result.get('status', 'unknown')}")
|
| 84 |
+
print(f"Current price: ${order_result.get('current_price', 'unknown')}")
|
| 85 |
+
else:
|
| 86 |
+
print(f"Order execution failed: {order_result.get('error', 'Unknown error')}")
|
| 87 |
+
else:
|
| 88 |
+
print(f"Account check completed: {order_result}")
|
| 89 |
+
|
| 90 |
+
return 0
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"Error in analysis: {str(e)}")
|
| 94 |
+
import traceback
|
| 95 |
+
traceback.print_exc()
|
| 96 |
+
return 1
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core packages
|
| 2 |
+
python-dotenv==1.1.0
|
| 3 |
+
crewai==0.108.0
|
| 4 |
+
openai==1.68.2
|
| 5 |
+
langchain==0.3.21
|
| 6 |
+
langchain-openai==0.2.14
|
| 7 |
+
pydantic==2.10.6
|
| 8 |
+
pydantic-settings==2.8.1
|
| 9 |
+
|
| 10 |
+
# Database and storage
|
| 11 |
+
chromadb==0.5.23
|
| 12 |
+
sqlalchemy==2.0.39
|
| 13 |
+
peewee==3.17.9
|
| 14 |
+
|
| 15 |
+
# Data analysis
|
| 16 |
+
pandas==2.2.3
|
| 17 |
+
numpy==2.2.4
|
| 18 |
+
pandas-ta==0.3.14b0
|
| 19 |
+
|
| 20 |
+
# Financial APIs
|
| 21 |
+
alpaca-py==0.39.1
|
| 22 |
+
yfinance==0.2.55
|
| 23 |
+
newsapi-python==0.2.7
|
| 24 |
+
|
| 25 |
+
# Data visualization
|
| 26 |
+
matplotlib==3.10.1
|
| 27 |
+
|
| 28 |
+
# HTTP and utilities
|
| 29 |
+
requests==2.32.3
|
| 30 |
+
aiohttp==3.11.14
|
| 31 |
+
httpx==0.27.2
|
| 32 |
+
tenacity==8.5.0
|
| 33 |
+
backoff==2.2.1
|
| 34 |
+
|
| 35 |
+
# Web scraping and text processing
|
| 36 |
+
beautifulsoup4==4.13.3
|
| 37 |
+
pysbd==0.3.4
|
| 38 |
+
regex==2024.11.6
|
| 39 |
+
|
| 40 |
+
# UI dependencies
|
| 41 |
+
gradio==5.23.1
|
| 42 |
+
fastapi==0.115.12
|
| 43 |
+
uvicorn==0.34.0
|
| 44 |
+
|
| 45 |
+
# Date and time utilities
|
| 46 |
+
python-dateutil==2.9.0.post0
|
| 47 |
+
pytz==2024.2
|
| 48 |
+
|
| 49 |
+
# Logging and error tracking
|
| 50 |
+
structlog==24.1.0
|
| 51 |
+
pythonjsonlogger==2.0.7
|
| 52 |
+
|
| 53 |
+
# Testing dependencies
|
| 54 |
+
pytest>=7.0.0
|
results/report_20250328_160002.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# BITCOIN TRADING RECOMMENDATION REPORT
|
| 3 |
+
## March 28, 2025 16:00
|
| 4 |
+
|
| 5 |
+
## EXECUTIVE SUMMARY
|
| 6 |
+
### βͺ HOLD with 0% confidence
|
| 7 |
+
### Allocation: 0% of portfolio
|
| 8 |
+
|
| 9 |
+
## TECHNICAL ANALYSIS
|
| 10 |
+
No technical analysis available
|
| 11 |
+
|
| 12 |
+
## MARKET CONTEXT
|
| 13 |
+
No market context available
|
| 14 |
+
|
| 15 |
+
## SENTIMENT ANALYSIS
|
| 16 |
+
No sentiment analysis available
|
| 17 |
+
|
| 18 |
+
## DECISION FACTORS
|
| 19 |
+
- Technical Factors: No impact data available
|
| 20 |
+
- Market Context: No impact data available
|
| 21 |
+
- Sentiment: No impact data available
|
| 22 |
+
|
| 23 |
+
## MARKET OUTLOOK
|
| 24 |
+
No market outlook available
|
| 25 |
+
|
| 26 |
+
## RISK ASSESSMENT
|
| 27 |
+
No risk assessment available
|
| 28 |
+
|
| 29 |
+
## DETAILED REASONING
|
| 30 |
+
Error parsing result: local variable 're' referenced before assignment
|
| 31 |
+
|
| 32 |
+
## ORDER EXECUTION
|
| 33 |
+
No order execution details available
|
run.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Activate virtual environment
|
| 4 |
+
if [ -d "venv" ]; then
|
| 5 |
+
source venv/bin/activate
|
| 6 |
+
echo "Virtual environment activated"
|
| 7 |
+
else
|
| 8 |
+
echo "Virtual environment not found. Creating one..."
|
| 9 |
+
python -m venv venv
|
| 10 |
+
source venv/bin/activate
|
| 11 |
+
pip install poetry
|
| 12 |
+
poetry install
|
| 13 |
+
echo "Virtual environment created and activated"
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
# Check if .env file exists
|
| 17 |
+
if [ ! -f ".env" ]; then
|
| 18 |
+
echo "Error: .env file not found."
|
| 19 |
+
echo "Please create a .env file with your API keys based on .env.example"
|
| 20 |
+
exit 1
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Parse command line arguments
|
| 24 |
+
if [ "$1" == "monitor" ]; then
|
| 25 |
+
echo "Starting Bitcoin analysis in monitoring mode..."
|
| 26 |
+
python -m src.crypto_analysis.main monitor
|
| 27 |
+
elif [ "$1" == "train" ]; then
|
| 28 |
+
ITERATIONS=${2:-1}
|
| 29 |
+
echo "Training Bitcoin analysis crew for $ITERATIONS iterations..."
|
| 30 |
+
python -m src.crypto_analysis.main train $ITERATIONS
|
| 31 |
+
else
|
| 32 |
+
echo "Running one-time Bitcoin analysis..."
|
| 33 |
+
python -m src.crypto_analysis.main
|
| 34 |
+
fi
|
run_trader.sh
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Configuration
|
| 4 |
+
MAX_RESTARTS=10
|
| 5 |
+
RESTART_DELAY=60 # seconds
|
| 6 |
+
LOG_FILE="trader_monitor.log"
|
| 7 |
+
APP_PATH="src/ui/app.py"
|
| 8 |
+
|
| 9 |
+
echo "Starting CrypticAI Trading Monitor at $(date)" | tee -a "$LOG_FILE"
|
| 10 |
+
echo "This script will automatically restart the application if it crashes" | tee -a "$LOG_FILE"
|
| 11 |
+
echo "Press CTRL+C to stop the monitor" | tee -a "$LOG_FILE"
|
| 12 |
+
echo "---------------------------------------------------------" | tee -a "$LOG_FILE"
|
| 13 |
+
|
| 14 |
+
restart_count=0
|
| 15 |
+
|
| 16 |
+
while [ $restart_count -lt $MAX_RESTARTS ]; do
|
| 17 |
+
echo "Starting CrypticAI Trading application (attempt $((restart_count+1)))" | tee -a "$LOG_FILE"
|
| 18 |
+
echo "$(date): Starting application" | tee -a "$LOG_FILE"
|
| 19 |
+
|
| 20 |
+
# Run the application
|
| 21 |
+
python "$APP_PATH"
|
| 22 |
+
|
| 23 |
+
# Check exit status
|
| 24 |
+
exit_status=$?
|
| 25 |
+
|
| 26 |
+
if [ $exit_status -eq 0 ]; then
|
| 27 |
+
echo "$(date): Application exited normally with status $exit_status" | tee -a "$LOG_FILE"
|
| 28 |
+
echo "Exiting monitor as requested" | tee -a "$LOG_FILE"
|
| 29 |
+
break
|
| 30 |
+
else
|
| 31 |
+
restart_count=$((restart_count+1))
|
| 32 |
+
echo "$(date): Application crashed with status $exit_status" | tee -a "$LOG_FILE"
|
| 33 |
+
|
| 34 |
+
if [ $restart_count -lt $MAX_RESTARTS ]; then
|
| 35 |
+
echo "Restarting in $RESTART_DELAY seconds... ($restart_count of $MAX_RESTARTS)" | tee -a "$LOG_FILE"
|
| 36 |
+
sleep $RESTART_DELAY
|
| 37 |
+
else
|
| 38 |
+
echo "Maximum restart attempts ($MAX_RESTARTS) reached. Giving up." | tee -a "$LOG_FILE"
|
| 39 |
+
fi
|
| 40 |
+
fi
|
| 41 |
+
done
|
| 42 |
+
|
| 43 |
+
echo "$(date): Monitor script terminated" | tee -a "$LOG_FILE"
|
run_ui.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
CrypticAI - Bitcoin Trading Dashboard
|
| 4 |
+
|
| 5 |
+
Run this script to start the Gradio UI for the CrypticAI Bitcoin Trading Dashboard.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
# Load environment variables
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# Check for required API keys
|
| 16 |
+
if not os.getenv("OPENAI_API_KEY"):
|
| 17 |
+
print("Error: OPENAI_API_KEY not found in environment variables")
|
| 18 |
+
print("Please set up your .env file with the required API keys")
|
| 19 |
+
sys.exit(1)
|
| 20 |
+
|
| 21 |
+
if not os.getenv("ALPACA_API_KEY") or not os.getenv("ALPACA_API_SECRET"):
|
| 22 |
+
print("Error: ALPACA_API_KEY or ALPACA_API_SECRET not found in environment variables")
|
| 23 |
+
print("Please set up your .env file with the required Alpaca API keys")
|
| 24 |
+
sys.exit(1)
|
| 25 |
+
|
| 26 |
+
# Import and run the Gradio app
|
| 27 |
+
from src.ui.app import app
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
print("=" * 80)
|
| 31 |
+
print("CrypticAI - Bitcoin Trading Dashboard")
|
| 32 |
+
print("=" * 80)
|
| 33 |
+
print("Starting Gradio UI...")
|
| 34 |
+
|
| 35 |
+
# Ensure results directory exists
|
| 36 |
+
os.makedirs("results", exist_ok=True)
|
| 37 |
+
|
| 38 |
+
# Launch the app
|
| 39 |
+
app.launch(share=False)
|
run_ui.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Script to run the CrypticAI Trading Dashboard UI
|
| 3 |
+
|
| 4 |
+
# Ensure virtual environment is activated if it exists
|
| 5 |
+
if [ -d "venv" ]; then
|
| 6 |
+
echo "Activating virtual environment..."
|
| 7 |
+
source venv/bin/activate
|
| 8 |
+
fi
|
| 9 |
+
|
| 10 |
+
# Check if python is available
|
| 11 |
+
if ! command -v python &> /dev/null; then
|
| 12 |
+
echo "Python not found. Please install Python 3.10 or higher."
|
| 13 |
+
exit 1
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
# Check if dependencies are installed
|
| 17 |
+
if [ ! -f "requirements_installed" ]; then
|
| 18 |
+
echo "Installing dependencies..."
|
| 19 |
+
pip install -r requirements.txt
|
| 20 |
+
touch requirements_installed
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Run the UI
|
| 24 |
+
echo "Starting CrypticAI Trading Dashboard..."
|
| 25 |
+
python run_ui.py
|
setup.sh
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
echo "Setting up Bitcoin Technical Analysis environment..."
|
| 4 |
+
|
| 5 |
+
# Ensure virtual environment is active
|
| 6 |
+
if [[ "$VIRTUAL_ENV" == "" ]]; then
|
| 7 |
+
echo "Virtual environment not active. Please activate it first with:"
|
| 8 |
+
echo "source venv/bin/activate"
|
| 9 |
+
exit 1
|
| 10 |
+
fi
|
| 11 |
+
|
| 12 |
+
# Install dependencies with fixed versions
|
| 13 |
+
echo "Installing dependencies with fixed versions..."
|
| 14 |
+
pip install -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Check if numpy has NaN attribute
|
| 17 |
+
echo "Checking numpy configuration..."
|
| 18 |
+
python -c "
|
| 19 |
+
import numpy as np
|
| 20 |
+
try:
|
| 21 |
+
# Try to access np.NaN
|
| 22 |
+
print(f'numpy.NaN exists: {np.NaN is np.nan}')
|
| 23 |
+
except AttributeError:
|
| 24 |
+
# If it doesn't exist, add it
|
| 25 |
+
print('Adding NaN to numpy...')
|
| 26 |
+
np.NaN = np.nan
|
| 27 |
+
"
|
| 28 |
+
|
| 29 |
+
echo "Setup complete. Running Bitcoin analysis..."
|
| 30 |
+
python main.py
|
src/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CrypticAI package for Bitcoin price sentiment analysis
|
| 3 |
+
"""
|
src/crypto_analysis/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Bitcoin price sentiment analysis with agentic workflow
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__version__ = "0.1.0"
|
src/crypto_analysis/config/agents.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
technical_strategist:
|
| 2 |
+
role: >
|
| 3 |
+
Bitcoin Technical Strategist
|
| 4 |
+
goal: >
|
| 5 |
+
Generate precise buy/sell signals for Bitcoin using 5-minute timeframe technical analysis
|
| 6 |
+
with RSI and Bollinger Bands indicators
|
| 7 |
+
backstory: >
|
| 8 |
+
You are a world-class technical analyst who specializes in cryptocurrency trading strategies.
|
| 9 |
+
You've developed a proprietary methodology combining RSI and Bollinger Bands on short timeframes
|
| 10 |
+
that consistently generates high-probability trading signals. Your expertise is in accurately
|
| 11 |
+
quantifying signal confidence and allocating portfolio percentages based on technical conditions.
|
| 12 |
+
|
| 13 |
+
initial_analyst:
|
| 14 |
+
role: >
|
| 15 |
+
Bitcoin Market Analyst
|
| 16 |
+
goal: >
|
| 17 |
+
Provide comprehensive initial analysis of Bitcoin market conditions
|
| 18 |
+
using technical indicators, news, and market trends
|
| 19 |
+
backstory: >
|
| 20 |
+
You are a seasoned Bitcoin market analyst with a decade of experience in cryptocurrency trading.
|
| 21 |
+
Your expertise lies in combining technical analysis with fundamental and sentiment data to create
|
| 22 |
+
a holistic view of current market conditions. You're known for your attention to detail and
|
| 23 |
+
ability to identify key market signals from multiple data sources.
|
| 24 |
+
|
| 25 |
+
reflection_analyst:
|
| 26 |
+
role: >
|
| 27 |
+
Bitcoin Market Strategist
|
| 28 |
+
goal: >
|
| 29 |
+
Critically evaluate the initial market analysis, challenge assumptions,
|
| 30 |
+
and identify additional factors that could influence Bitcoin's price
|
| 31 |
+
backstory: >
|
| 32 |
+
You are a strategic thinker with a background in both market analysis and behavioral finance.
|
| 33 |
+
Your strength is your ability to step back and see the bigger picture, identifying potential
|
| 34 |
+
blind spots in analysis and considering alternative viewpoints. You excel at questioning
|
| 35 |
+
assumptions and understanding how different market factors interact with each other.
|
| 36 |
+
|
| 37 |
+
synthesis_analyst:
|
| 38 |
+
role: >
|
| 39 |
+
Bitcoin Decision Strategist
|
| 40 |
+
goal: >
|
| 41 |
+
Synthesize all available information to produce a final recommendation
|
| 42 |
+
with confidence score and suggested portfolio allocation
|
| 43 |
+
backstory: >
|
| 44 |
+
You are a decisive investment strategist with exceptional skills in weighing different
|
| 45 |
+
market perspectives and making clear, actionable recommendations. Your background spans
|
| 46 |
+
both traditional finance and cryptocurrency markets. You are known for your balanced judgment
|
| 47 |
+
and ability to quantify uncertainty into precise confidence levels that guide portfolio allocation
|
| 48 |
+
decisions.
|
src/crypto_analysis/config/tasks.yaml
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
technical_analysis:
|
| 2 |
+
description: >
|
| 3 |
+
Analyze Bitcoin using the 5-minute timeframe trading strategy based on RSI and Bollinger Bands.
|
| 4 |
+
Following these specific rules:
|
| 5 |
+
|
| 6 |
+
1. Use the Technical Analysis Strategy Tool to fetch Bitcoin price data and calculate indicators
|
| 7 |
+
2. For Buy Signals:
|
| 8 |
+
- RSI below 40 (mildly oversold)
|
| 9 |
+
- Price near or touching the lower Bollinger Band (BBL)
|
| 10 |
+
- Preferably showing signs of bouncing up from the lower band
|
| 11 |
+
3. For Sell Signals:
|
| 12 |
+
- RSI above 60 (mildly overbought)
|
| 13 |
+
- Price near or touching the upper Bollinger Band (BBU)
|
| 14 |
+
- Preferably showing signs of reversing down from the upper band
|
| 15 |
+
4. If conditions don't clearly indicate buy or sell, recommend hold
|
| 16 |
+
|
| 17 |
+
Provide exact numerical values for all indicators and precisely calculate the position of price
|
| 18 |
+
relative to the Bollinger Bands to ensure accurate signals.
|
| 19 |
+
|
| 20 |
+
expected_output: >
|
| 21 |
+
A precise technical analysis report that includes:
|
| 22 |
+
- Current Bitcoin price
|
| 23 |
+
- Current RSI value
|
| 24 |
+
- Current Bollinger Band values (upper, middle, lower)
|
| 25 |
+
- Clear signal: BUY, SELL, or HOLD
|
| 26 |
+
- Confidence level (0-100%)
|
| 27 |
+
- Recommended portfolio allocation percentage
|
| 28 |
+
- Detailed reasoning behind the signal
|
| 29 |
+
- Exact calculations showing how the signal was determined
|
| 30 |
+
|
| 31 |
+
initial_analysis:
|
| 32 |
+
description: >
|
| 33 |
+
Conduct an initial analysis of Bitcoin's current market conditions by examining technical indicators,
|
| 34 |
+
sentiment from news and social media, overall market context, and the technical strategy signal. You must:
|
| 35 |
+
1. Consider the technical trading signal from the 5-minute timeframe strategy
|
| 36 |
+
2. Analyze broader technical indicators beyond the 5-minute timeframe
|
| 37 |
+
3. Review recent news and sentiment to gauge market perception
|
| 38 |
+
4. Consider broader cryptocurrency market trends that might impact Bitcoin
|
| 39 |
+
5. Determine if news and broader market trends support or contradict the trading signal
|
| 40 |
+
|
| 41 |
+
expected_output: >
|
| 42 |
+
A comprehensive initial market analysis report that includes:
|
| 43 |
+
- Current Bitcoin price and recent price action
|
| 44 |
+
- An assessment of the technical trading signal's validity in the current market
|
| 45 |
+
- Additional technical indicator readings and what they suggest
|
| 46 |
+
- Market sentiment based on news analysis
|
| 47 |
+
- Whether the news sentiment aligns with or contradicts the technical signals
|
| 48 |
+
- Key market factors currently influencing Bitcoin
|
| 49 |
+
- Contextual data from the broader cryptocurrency market
|
| 50 |
+
|
| 51 |
+
reflection:
|
| 52 |
+
description: >
|
| 53 |
+
Review both the technical trading signal and the initial market analysis to provide a critical reflection.
|
| 54 |
+
Your task is to:
|
| 55 |
+
1. Evaluate if the technical strategy signal (from the 5-minute RSI/BB strategy) is likely reliable
|
| 56 |
+
2. Identify potential limitations of the short-timeframe technical strategy
|
| 57 |
+
3. Assess if important market factors were missed in the initial analysis
|
| 58 |
+
4. Consider how news sentiment might override or reinforce the technical signals
|
| 59 |
+
5. Analyze if the recommended portfolio allocation from the technical strategy is appropriate
|
| 60 |
+
6. Suggest additional data or market factors that should be considered
|
| 61 |
+
|
| 62 |
+
expected_output: >
|
| 63 |
+
A reflective assessment that:
|
| 64 |
+
- Critically evaluates the strengths and limitations of the 5-minute technical strategy
|
| 65 |
+
- Addresses the appropriateness of the confidence level assigned by the technical strategy
|
| 66 |
+
- Evaluates if the initial analysis properly integrated the technical signal with other factors
|
| 67 |
+
- Identifies any contradictory signals between technical analysis and news/sentiment
|
| 68 |
+
- Highlights additional market factors that could affect Bitcoin price in the short term
|
| 69 |
+
- Suggests improvements to the technical strategy or initial analysis approach
|
| 70 |
+
|
| 71 |
+
final_synthesis:
|
| 72 |
+
description: >
|
| 73 |
+
Synthesize all three analyses (technical strategy, initial analysis, and reflection) to produce
|
| 74 |
+
a final recommendation. Based on all available information, you should:
|
| 75 |
+
1. Determine if the technical trading signal should be followed, modified, or rejected
|
| 76 |
+
2. Consider how news sentiment and broader market context affect the signal's reliability
|
| 77 |
+
3. Address points raised in the reflection about potential limitations or blind spots
|
| 78 |
+
4. Make a final buy, sell, or hold recommendation
|
| 79 |
+
5. Assign your own confidence level that may differ from the technical strategy
|
| 80 |
+
6. Adjust the recommended portfolio allocation if necessary
|
| 81 |
+
7. Provide a comprehensive explanation for your final decision
|
| 82 |
+
|
| 83 |
+
expected_output: >
|
| 84 |
+
A final recommendation that includes:
|
| 85 |
+
- Current Bitcoin Price
|
| 86 |
+
- Original Technical Signal (buy, sell, or hold)
|
| 87 |
+
- Technical Signal Confidence (from strategy)
|
| 88 |
+
- Final Recommendation (buy, sell, or hold) - may differ from technical signal
|
| 89 |
+
- Final Confidence Score (0-100%)
|
| 90 |
+
- Final Portfolio Allocation Percentage
|
| 91 |
+
- Time horizon for the recommendation
|
| 92 |
+
- Reasoning that explicitly addresses:
|
| 93 |
+
* Why you followed or deviated from the technical signal
|
| 94 |
+
* How news and sentiment influenced your decision
|
| 95 |
+
* How you addressed concerns raised in the reflection
|
| 96 |
+
* Key risks that could invalidate your recommendation
|
src/crypto_analysis/crew.py
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import logging
|
| 3 |
+
import functools
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
from crewai import Agent, Crew, Process, Task
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from src.crypto_analysis.tools.bitcoin_tools import YahooBitcoinDataTool, RealBitcoinNewsTool
|
| 11 |
+
from src.crypto_analysis.tools.technical_tools import TechnicalAnalysisStrategy
|
| 12 |
+
from src.crypto_analysis.tools.order_tools import AlpacaCryptoOrderTool
|
| 13 |
+
from src.crypto_analysis.tools.yahoo_tools import YahooCryptoMarketTool
|
| 14 |
+
|
| 15 |
+
# Import your preferred LLM
|
| 16 |
+
import openai
|
| 17 |
+
from langchain_openai import ChatOpenAI
|
| 18 |
+
|
| 19 |
+
# Set up logging
|
| 20 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 21 |
+
logger = logging.getLogger("bitcoin_crew")
|
| 22 |
+
|
| 23 |
+
# Set up OpenAI client
|
| 24 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 25 |
+
llm = ChatOpenAI(model="gpt-4o")
|
| 26 |
+
|
| 27 |
+
class BitcoinAnalysisCrew:
|
| 28 |
+
"""A simplified Bitcoin analysis crew with reflection and synthesis"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, timeframe="5m", max_allocation=100):
|
| 31 |
+
"""
|
| 32 |
+
Initialize the Bitcoin analysis crew
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
timeframe (str): Timeframe for technical analysis (e.g., "1m", "5m", "15m", "1h", "4h", "1d")
|
| 36 |
+
max_allocation (int): Maximum portfolio allocation percentage allowed (1-100)
|
| 37 |
+
"""
|
| 38 |
+
# Store user preferences
|
| 39 |
+
self.timeframe = timeframe
|
| 40 |
+
self.max_allocation = min(max(1, max_allocation), 100) # Ensure between 1-100
|
| 41 |
+
|
| 42 |
+
self.bitcoin_data_tool = YahooBitcoinDataTool(
|
| 43 |
+
max_retries=3,
|
| 44 |
+
backoff_factor=2.0,
|
| 45 |
+
timeout=30,
|
| 46 |
+
cache_duration_minutes=15,
|
| 47 |
+
timeframe=self.timeframe # Pass timeframe to data tool
|
| 48 |
+
)
|
| 49 |
+
self.bitcoin_news_tool = RealBitcoinNewsTool()
|
| 50 |
+
self.technical_strategy_tool = TechnicalAnalysisStrategy(
|
| 51 |
+
timeframe=self.timeframe # Pass timeframe to technical strategy tool
|
| 52 |
+
)
|
| 53 |
+
self.order_tool = AlpacaCryptoOrderTool()
|
| 54 |
+
self.crypto_market_tool = YahooCryptoMarketTool(
|
| 55 |
+
max_retries=3,
|
| 56 |
+
backoff_factor=2.0,
|
| 57 |
+
timeout=30,
|
| 58 |
+
cache_duration_minutes=30
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Save agent instances for reuse
|
| 62 |
+
self._technical_agent = None
|
| 63 |
+
self._analyst_agent = None
|
| 64 |
+
self._reflection_agent = None
|
| 65 |
+
self._synthesis_agent = None
|
| 66 |
+
|
| 67 |
+
# Track tool states for error handling
|
| 68 |
+
self.tool_states = {
|
| 69 |
+
"bitcoin_data": {"status": "not_started", "error": None},
|
| 70 |
+
"bitcoin_news": {"status": "not_started", "error": None},
|
| 71 |
+
"technical_strategy": {"status": "not_started", "error": None},
|
| 72 |
+
"crypto_market": {"status": "not_started", "error": None},
|
| 73 |
+
"order_tool": {"status": "not_started", "error": None}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
# Patch tools for monitoring
|
| 77 |
+
self._patch_tools_for_monitoring()
|
| 78 |
+
|
| 79 |
+
logger.info(f"Initialized Bitcoin analysis crew with timeframe={timeframe}, max_allocation={max_allocation}%")
|
| 80 |
+
|
| 81 |
+
def create_technical_analyst_agent(self) -> Agent:
|
| 82 |
+
"""Creates a Technical Analysis agent for Bitcoin"""
|
| 83 |
+
if self._technical_agent is not None:
|
| 84 |
+
return self._technical_agent
|
| 85 |
+
|
| 86 |
+
self._technical_agent = Agent(
|
| 87 |
+
name="Bitcoin Technical Strategist",
|
| 88 |
+
role="Bitcoin technical trading strategist",
|
| 89 |
+
goal=f"Analyze Bitcoin technical indicators using {self.timeframe} timeframe data to generate accurate buy/sell signals",
|
| 90 |
+
backstory=f"""You are an expert in technical analysis for Bitcoin trading.
|
| 91 |
+
You specialize in analyzing {self.timeframe} price patterns using key indicators
|
| 92 |
+
like RSI and Bollinger Bands to identify high-probability trading opportunities.
|
| 93 |
+
You pride yourself on providing clear, data-driven trading signals
|
| 94 |
+
with confidence levels and precise allocation recommendations.
|
| 95 |
+
|
| 96 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 97 |
+
You MUST NOT recommend allocations higher than this, regardless of how strong the signal is.""",
|
| 98 |
+
verbose=True,
|
| 99 |
+
llm=llm,
|
| 100 |
+
tools=[self.technical_strategy_tool]
|
| 101 |
+
)
|
| 102 |
+
return self._technical_agent
|
| 103 |
+
|
| 104 |
+
def create_analyst_agent(self) -> Agent:
|
| 105 |
+
"""Creates a Bitcoin analyst agent for initial analysis"""
|
| 106 |
+
if self._analyst_agent is not None:
|
| 107 |
+
return self._analyst_agent
|
| 108 |
+
|
| 109 |
+
self._analyst_agent = Agent(
|
| 110 |
+
name="Bitcoin Initial Analyst",
|
| 111 |
+
role="Bitcoin technical analyst",
|
| 112 |
+
goal=f"Analyze Bitcoin technical data on {self.timeframe} timeframe, news, and technical strategy signals to provide a holistic assessment",
|
| 113 |
+
backstory=f"""You are a technical analyst with expertise in Bitcoin price patterns
|
| 114 |
+
and market indicators on the {self.timeframe} timeframe. You focus on data-driven analysis while also incorporating
|
| 115 |
+
news sentiment and technical trading signals from specialized strategy models.
|
| 116 |
+
|
| 117 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 118 |
+
You MUST respect this limit in your analysis and recommendations.
|
| 119 |
+
|
| 120 |
+
IMPORTANT: You must check for error reports in tool outputs. If a tool returns an 'error' field,
|
| 121 |
+
you should acknowledge the error, explain its potential impact on your analysis, and adjust your
|
| 122 |
+
approach accordingly. Do not simply proceed as if the tool succeeded.
|
| 123 |
+
|
| 124 |
+
If multiple tools return errors, focus on what data is available and explicitly state the limitations
|
| 125 |
+
of your analysis due to missing data.""",
|
| 126 |
+
verbose=True,
|
| 127 |
+
llm=llm,
|
| 128 |
+
tools=[self.bitcoin_data_tool, self.bitcoin_news_tool, self.crypto_market_tool]
|
| 129 |
+
)
|
| 130 |
+
return self._analyst_agent
|
| 131 |
+
|
| 132 |
+
def create_reflection_agent(self) -> Agent:
|
| 133 |
+
"""Creates a reflection agent to evaluate the initial analysis"""
|
| 134 |
+
if self._reflection_agent is not None:
|
| 135 |
+
return self._reflection_agent
|
| 136 |
+
|
| 137 |
+
self._reflection_agent = Agent(
|
| 138 |
+
name="Bitcoin Reflection Analyst",
|
| 139 |
+
role="Bitcoin market sentiment analyst",
|
| 140 |
+
goal="Evaluate initial analysis and technical strategy signals to provide a market sentiment perspective",
|
| 141 |
+
backstory=f"""You are a market sentiment specialist who analyzes news and social trends
|
| 142 |
+
to determine broader market sentiment around Bitcoin. You also consider technical signals
|
| 143 |
+
and initial analysis to form a comprehensive view of market conditions.
|
| 144 |
+
|
| 145 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 146 |
+
Any recommendation you make should respect this limit.
|
| 147 |
+
|
| 148 |
+
IMPORTANT: If you encounter errors or limitations in earlier analyses due to tool failures,
|
| 149 |
+
acknowledge these limitations and adapt your reflection accordingly. Be explicit about how
|
| 150 |
+
missing data affects the reliability of your assessment.""",
|
| 151 |
+
verbose=True,
|
| 152 |
+
llm=llm,
|
| 153 |
+
tools=[self.bitcoin_news_tool]
|
| 154 |
+
)
|
| 155 |
+
return self._reflection_agent
|
| 156 |
+
|
| 157 |
+
def create_synthesis_agent(self) -> Agent:
|
| 158 |
+
"""Creates a synthesis agent to provide final recommendation"""
|
| 159 |
+
if self._synthesis_agent is not None:
|
| 160 |
+
return self._synthesis_agent
|
| 161 |
+
|
| 162 |
+
self._synthesis_agent = Agent(
|
| 163 |
+
name="Bitcoin Synthesis Analyst",
|
| 164 |
+
role="Senior cryptocurrency investment advisor",
|
| 165 |
+
goal="Synthesize all analysis (technical strategy, initial, and reflection) to provide final recommendation and execute trades with comprehensive reasoning",
|
| 166 |
+
backstory=f"""You are a seasoned investment advisor who specializes in cryptocurrency.
|
| 167 |
+
You analyze technical signals, market sentiment, and fundamental factors to provide
|
| 168 |
+
balanced investment advice with high confidence levels and specific allocation percentages.
|
| 169 |
+
You can also execute trades on behalf of clients based on your final recommendations.
|
| 170 |
+
|
| 171 |
+
You are known for providing highly detailed and comprehensive analysis reports that break down
|
| 172 |
+
each factor that influenced your decision, quantifying the impact of each element on your final recommendation.
|
| 173 |
+
|
| 174 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 175 |
+
You MUST NOT exceed this limit in your allocation recommendation, regardless of signal strength.
|
| 176 |
+
|
| 177 |
+
IMPORTANT: You must acknowledge and account for any data limitations or tool failures in your synthesis.
|
| 178 |
+
If there were errors in previous analyses, explicitly state how these affect your final recommendation's
|
| 179 |
+
reliability and adjust your confidence levels accordingly.
|
| 180 |
+
|
| 181 |
+
IMPORTANT TOOL USAGE - When using the Alpaca Crypto Order Tool:
|
| 182 |
+
1. For BUY orders, use:
|
| 183 |
+
{{"action": "buy", "symbol": "BTC/USD", "allocation_percentage": X, "confidence": Y}}
|
| 184 |
+
where X is your recommended allocation percentage (NOT EXCEEDING {self.max_allocation}%)
|
| 185 |
+
The tool will automatically convert the allocation_percentage into the appropriate BTC quantity.
|
| 186 |
+
2. For SELL orders, use:
|
| 187 |
+
{{"action": "sell", "symbol": "BTC/USD", "allocation_percentage": X, "confidence": Y}}
|
| 188 |
+
where X is your recommended allocation percentage (NOT EXCEEDING {self.max_allocation}%)
|
| 189 |
+
The tool will automatically calculate how many BTC to sell based on your portfolio.
|
| 190 |
+
3. For checking account (HOLD), use:
|
| 191 |
+
{{"action": "check"}}
|
| 192 |
+
""",
|
| 193 |
+
verbose=True,
|
| 194 |
+
llm=llm,
|
| 195 |
+
tools=[self.order_tool]
|
| 196 |
+
)
|
| 197 |
+
return self._synthesis_agent
|
| 198 |
+
|
| 199 |
+
def create_technical_strategy_task(self, strategy_text=None) -> Task:
|
| 200 |
+
"""Creates technical strategy analysis task"""
|
| 201 |
+
# Generate description using provided strategy or default
|
| 202 |
+
if strategy_text:
|
| 203 |
+
description = f"""Analyze Bitcoin's technical indicators on the {self.timeframe} timeframe to generate a buy/sell signal based on the following strategy:
|
| 204 |
+
|
| 205 |
+
{strategy_text}
|
| 206 |
+
|
| 207 |
+
Use the Technical Analysis Strategy Tool to get current indicator values and analyze them according to the strategy.
|
| 208 |
+
|
| 209 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 210 |
+
Do NOT recommend allocations above this limit, regardless of how strong the signal is.
|
| 211 |
+
|
| 212 |
+
IMPORTANT: If the tool returns an error (look for an "error" field in the response),
|
| 213 |
+
you must:
|
| 214 |
+
1. Acknowledge the error in your analysis
|
| 215 |
+
2. Explain how it impacts your ability to provide a reliable signal
|
| 216 |
+
3. If possible, make a recommendation based on any partial data available
|
| 217 |
+
4. Set confidence lower to reflect the uncertainty
|
| 218 |
+
|
| 219 |
+
Your analysis should include:
|
| 220 |
+
- Clear buy, sell, or hold signal
|
| 221 |
+
- Confidence level as a percentage (0-100%)
|
| 222 |
+
- Recommended portfolio allocation percentage (maximum {self.max_allocation}%)
|
| 223 |
+
- Detailed reasoning explaining the signal
|
| 224 |
+
- Current values for all relevant indicators
|
| 225 |
+
- Any error messages or data limitations
|
| 226 |
+
"""
|
| 227 |
+
else:
|
| 228 |
+
# Fall back to default strategy if none provided
|
| 229 |
+
description = f"""Analyze Bitcoin's technical indicators on the {self.timeframe} timeframe to generate a buy/sell signal.
|
| 230 |
+
|
| 231 |
+
1. Use the Technical Analysis Strategy Tool to analyze RSI and Bollinger Bands on the {self.timeframe} timeframe
|
| 232 |
+
2. Evaluate if current conditions meet the criteria for a buy signal:
|
| 233 |
+
- RSI below 40
|
| 234 |
+
- Price near or touching the lower Bollinger Band
|
| 235 |
+
- Signs of a price bounce from the lower band
|
| 236 |
+
3. Evaluate if current conditions meet the criteria for a sell signal:
|
| 237 |
+
- RSI above 60
|
| 238 |
+
- Price near or touching the upper Bollinger Band
|
| 239 |
+
- Signs of price reversal from the upper band
|
| 240 |
+
4. If neither buy nor sell conditions are met, recommend hold
|
| 241 |
+
|
| 242 |
+
IMPORTANT: The user has set a maximum allocation of {self.max_allocation}% of their portfolio.
|
| 243 |
+
Do NOT recommend allocations above this limit.
|
| 244 |
+
|
| 245 |
+
IMPORTANT: If the tool returns an error (look for an "error" field in the response),
|
| 246 |
+
you must:
|
| 247 |
+
1. Acknowledge the error in your analysis
|
| 248 |
+
2. Explain how it impacts your ability to provide a reliable signal
|
| 249 |
+
3. If possible, make a recommendation based on any partial data available
|
| 250 |
+
4. Set confidence lower to reflect the uncertainty
|
| 251 |
+
|
| 252 |
+
Your analysis should include:
|
| 253 |
+
- Clear buy, sell, or hold signal
|
| 254 |
+
- Confidence level as a percentage (0-100%)
|
| 255 |
+
- Recommended portfolio allocation percentage (not exceeding {self.max_allocation}%)
|
| 256 |
+
- Detailed reasoning explaining the signal
|
| 257 |
+
- Current values for all relevant indicators
|
| 258 |
+
- Any error messages or data limitations
|
| 259 |
+
"""
|
| 260 |
+
|
| 261 |
+
return Task(
|
| 262 |
+
description=description,
|
| 263 |
+
agent=self.create_technical_analyst_agent(),
|
| 264 |
+
expected_output="Technical strategy analysis with clear signal, confidence level, and allocation recommendation"
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
def create_initial_task(self, technical_task) -> Task:
|
| 268 |
+
"""Creates initial analysis task"""
|
| 269 |
+
return Task(
|
| 270 |
+
description="""Analyze Bitcoin's current market situation using price data, news, technical strategy signals, and broader crypto market context.
|
| 271 |
+
|
| 272 |
+
1. Get the latest Bitcoin price data and identify the trend
|
| 273 |
+
2. Check recent news that might impact Bitcoin price
|
| 274 |
+
3. Consider the technical strategy signals provided
|
| 275 |
+
4. Analyze the broader cryptocurrency market context using the Yahoo Finance Crypto Market Tool
|
| 276 |
+
5. Provide an initial holistic assessment
|
| 277 |
+
|
| 278 |
+
IMPORTANT: For each tool you use, check if there's an "error" field in the response.
|
| 279 |
+
If any tool returns an error:
|
| 280 |
+
1. Acknowledge the error explicitly
|
| 281 |
+
2. Explain how it affects your analysis
|
| 282 |
+
3. Adjust your conclusions accordingly
|
| 283 |
+
4. Use whatever partial data is available
|
| 284 |
+
|
| 285 |
+
Your analysis should include:
|
| 286 |
+
- Current Bitcoin Price (if available)
|
| 287 |
+
- Recent Price Trend (bullish or bearish, if data available)
|
| 288 |
+
- Technical Signals (including those from the strategy)
|
| 289 |
+
- Broader Crypto Market Context (including BTC dominance and overall market trend, if available)
|
| 290 |
+
- Key News Impact
|
| 291 |
+
- Data Limitations (explain any missing or potentially unreliable data)
|
| 292 |
+
- How the news sentiment and market context align or conflict with technical signals
|
| 293 |
+
""",
|
| 294 |
+
agent=self.create_analyst_agent(),
|
| 295 |
+
context=[technical_task],
|
| 296 |
+
expected_output="Initial holistic assessment of Bitcoin's current market situation"
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
def create_reflection_task(self, technical_task, initial_task) -> Task:
|
| 300 |
+
"""Creates reflection task building on initial analysis"""
|
| 301 |
+
return Task(
|
| 302 |
+
description="""Review the technical strategy signals and initial Bitcoin analysis to add market sentiment perspective.
|
| 303 |
+
|
| 304 |
+
1. Evaluate the news sentiment around Bitcoin using the Bitcoin News Tool
|
| 305 |
+
2. Consider if technical signals and initial analysis align with market sentiment
|
| 306 |
+
3. Identify any potential gaps or oversights in the technical strategy or initial analysis
|
| 307 |
+
4. Evaluate if the technical strategy confidence level seems appropriate given broader market context
|
| 308 |
+
|
| 309 |
+
IMPORTANT: If you encounter errors from tools or limitations noted in the previous analyses,
|
| 310 |
+
acknowledge these explicitly and explain how they affect your assessment.
|
| 311 |
+
|
| 312 |
+
Your reflection should include:
|
| 313 |
+
- Sentiment Assessment (positive, negative, or neutral)
|
| 314 |
+
- Agreement or Disagreement with Technical Strategy Signals
|
| 315 |
+
- Agreement or Disagreement with Initial Analysis
|
| 316 |
+
- Data Reliability Assessment (mention any tool errors or data limitations)
|
| 317 |
+
- Additional Insights Not Covered in Previous Analyses
|
| 318 |
+
""",
|
| 319 |
+
agent=self.create_reflection_agent(),
|
| 320 |
+
context=[technical_task, initial_task],
|
| 321 |
+
expected_output="Reflection on technical strategy and initial analysis with sentiment perspective"
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
def create_synthesis_task(self, technical_task, initial_task, reflection_task) -> Task:
|
| 325 |
+
"""Creates synthesis task for final recommendation"""
|
| 326 |
+
return Task(
|
| 327 |
+
description=f"""Synthesize all analyses (technical strategy, initial, and reflection) to provide a final investment recommendation and execute the trade.
|
| 328 |
+
|
| 329 |
+
1. Consider the technical strategy signals (buy/sell/hold) with HIGH PRIORITY
|
| 330 |
+
- Use the technical strategy's confidence and allocation as a BASELINE
|
| 331 |
+
- Adjust confidence based on any tool errors or data limitations noted
|
| 332 |
+
2. Evaluate the initial holistic assessment
|
| 333 |
+
- Pay special attention to the broader crypto market context and BTC dominance
|
| 334 |
+
- Consider how Bitcoin's performance compares to other cryptocurrencies
|
| 335 |
+
3. Incorporate the sentiment perspective from the reflection
|
| 336 |
+
4. Make a final recommendation that integrates all three perspectives
|
| 337 |
+
- If technical and news analyses conflict, split the difference
|
| 338 |
+
- NEVER output a confidence of 0% for a BUY or SELL signal
|
| 339 |
+
- If recommending BUY or SELL, the minimum confidence should be 30%
|
| 340 |
+
- If recommending BUY or SELL, the minimum allocation should be 5%
|
| 341 |
+
- The MAXIMUM allocation must NOT exceed {self.max_allocation}% (user's set limit)
|
| 342 |
+
- IMPORTANT: If there were significant tool errors, reduce confidence accordingly
|
| 343 |
+
5. Provide your own confidence level and allocation percentage
|
| 344 |
+
6. EXECUTE THE TRADE using the Alpaca Crypto Order Tool as the very last step:
|
| 345 |
+
- IMPORTANT: Always use the symbol format "BTC/USD" with the slash, not "BTCUSD"
|
| 346 |
+
- You MUST use allocation_percentage, which represents what percentage of the portfolio to allocate
|
| 347 |
+
- For BUY orders: The tool will convert the allocation_percentage into the appropriate BTC quantity
|
| 348 |
+
Example: {{"action": "buy", "symbol": "BTC/USD", "allocation_percentage": N, "confidence": M}}
|
| 349 |
+
Where N is between 5 and {self.max_allocation}, and M is between 30 and 100
|
| 350 |
+
- For SELL orders: The tool will calculate how many BTC to sell based on your allocation_percentage
|
| 351 |
+
Example: {{"action": "sell", "symbol": "BTC/USD", "allocation_percentage": N, "confidence": M}}
|
| 352 |
+
Where N is between 5 and {self.max_allocation}, and M is between 30 and 100
|
| 353 |
+
- For HOLD recommendation: Check the account status only
|
| 354 |
+
Example: {{"action": "check"}}
|
| 355 |
+
- DO NOT try to calculate the exact BTC quantity yourself - the tool will do this automatically
|
| 356 |
+
- DO NOT include additional parameters beyond what's shown in the examples
|
| 357 |
+
- VERIFY that your input matches EXACTLY one of the example formats above
|
| 358 |
+
|
| 359 |
+
EXAMPLES OF INCORRECT USAGE (DO NOT DO THESE):
|
| 360 |
+
- DO NOT use: {{"action": "buy", "quantity": 0.001}} β Don't specify quantity directly
|
| 361 |
+
- DO NOT use: {{"action": "buy", "symbol...": 15, "confidence": 75}} β Missing quotes around symbol
|
| 362 |
+
- DO NOT use: {{"action": "buy", "symbol": "BTC/USD"}} β Missing allocation_percentage
|
| 363 |
+
- DO NOT use: {{"action": "buy", "symbol": "BTCUSD", "allocation_percentage": 15}} β Wrong symbol format
|
| 364 |
+
- DO NOT use: {{"action": "buy", "symbol": "BTC/USD", "quantity": 0.001, "allocation_percentage": 15}} β Using both quantity and allocation
|
| 365 |
+
|
| 366 |
+
Your final recommendation must be EXTREMELY DETAILED and should include:
|
| 367 |
+
- Current Bitcoin Price: (latest price if available)
|
| 368 |
+
- Technical Signal: (from strategy: buy, sell, or hold)
|
| 369 |
+
- Technical Confidence: (from strategy: 0-100%)
|
| 370 |
+
- Technical Allocation: (from strategy: percentage)
|
| 371 |
+
- Tool Error Assessment: (explicitly describe any tool errors encountered in the analysis process and how they affected your recommendation)
|
| 372 |
+
- Data Reliability: (assess the reliability of the data used for this recommendation)
|
| 373 |
+
- Technical Analysis Summary: (detailed explanation of the technical indicators at {self.timeframe} timeframe)
|
| 374 |
+
- Initial Analysis Summary: (detailed explanation of the broader market context)
|
| 375 |
+
- Reflection Analysis Summary: (detailed explanation of market sentiment)
|
| 376 |
+
- Importance Weighting: (explicitly state how you weighted each analysis: technical, initial, and reflection)
|
| 377 |
+
- Impact of Technical Factors: (explicit percentage impact of technical factors on your decision, with detailed explanation)
|
| 378 |
+
- Impact of Market Context: (explicit percentage impact of market context on your decision, with detailed explanation)
|
| 379 |
+
- Impact of Sentiment: (explicit percentage impact of sentiment on your decision, with detailed explanation)
|
| 380 |
+
- Final Recommendation: (buy, sell, or hold)
|
| 381 |
+
- Final Confidence: (0-100%)
|
| 382 |
+
- Allocation Percentage: (how much of portfolio to allocate, NOT EXCEEDING {self.max_allocation}%)
|
| 383 |
+
- Detailed Reasoning: (comprehensive explanation of how you arrived at your final recommendation, including how you reconciled any conflicting signals)
|
| 384 |
+
- Market Outlook: (your assessment of the likely short-term direction of Bitcoin price)
|
| 385 |
+
- Risk Assessment: (evaluation of the risk associated with your recommendation)
|
| 386 |
+
- Trade Execution: (details of the order executed or account status check)
|
| 387 |
+
""",
|
| 388 |
+
agent=self.create_synthesis_agent(),
|
| 389 |
+
context=[technical_task, initial_task, reflection_task],
|
| 390 |
+
expected_output="Final Bitcoin investment recommendation with detailed analysis breakdown and trade execution details"
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def run_analysis(self, strategy_text=None, timeframe=None, max_allocation=None) -> Dict[str, Any]:
|
| 394 |
+
"""
|
| 395 |
+
Run the Bitcoin analysis workflow with reflection and synthesis
|
| 396 |
+
|
| 397 |
+
Args:
|
| 398 |
+
strategy_text: Custom trading strategy text from UI
|
| 399 |
+
timeframe: Optional override for analysis timeframe (e.g. "1m", "5m", "15m", "1h", "4h", "1d")
|
| 400 |
+
max_allocation: Optional override for maximum portfolio allocation percentage (1-100)
|
| 401 |
+
|
| 402 |
+
Returns:
|
| 403 |
+
Dictionary with analysis results and trading recommendation
|
| 404 |
+
"""
|
| 405 |
+
try:
|
| 406 |
+
# Update timeframe and max_allocation if provided
|
| 407 |
+
if timeframe is not None:
|
| 408 |
+
self.timeframe = timeframe
|
| 409 |
+
# Update tools with new timeframe
|
| 410 |
+
self.bitcoin_data_tool.timeframe = timeframe
|
| 411 |
+
self.technical_strategy_tool.timeframe = timeframe
|
| 412 |
+
logger.info(f"Updated timeframe for analysis to {timeframe}")
|
| 413 |
+
|
| 414 |
+
if max_allocation is not None:
|
| 415 |
+
self.max_allocation = min(max(1, max_allocation), 100) # Ensure between 1-100
|
| 416 |
+
logger.info(f"Updated maximum allocation to {self.max_allocation}%")
|
| 417 |
+
|
| 418 |
+
logger.info(f"Starting Bitcoin analysis workflow with timeframe={self.timeframe}, max_allocation={self.max_allocation}%")
|
| 419 |
+
|
| 420 |
+
# Reset tool states
|
| 421 |
+
for key in self.tool_states:
|
| 422 |
+
self.tool_states[key] = {"status": "not_started", "error": None}
|
| 423 |
+
|
| 424 |
+
# Create tasks
|
| 425 |
+
logger.info("Creating tasks")
|
| 426 |
+
technical_task = self.create_technical_strategy_task(strategy_text)
|
| 427 |
+
initial_task = self.create_initial_task(technical_task)
|
| 428 |
+
reflection_task = self.create_reflection_task(technical_task, initial_task)
|
| 429 |
+
synthesis_task = self.create_synthesis_task(technical_task, initial_task, reflection_task)
|
| 430 |
+
|
| 431 |
+
# Create the crew with all agents and tasks
|
| 432 |
+
logger.info("Creating crew")
|
| 433 |
+
crew = Crew(
|
| 434 |
+
agents=[
|
| 435 |
+
self.create_technical_analyst_agent(),
|
| 436 |
+
self.create_analyst_agent(),
|
| 437 |
+
self.create_reflection_agent(),
|
| 438 |
+
self.create_synthesis_agent()
|
| 439 |
+
],
|
| 440 |
+
tasks=[technical_task, initial_task, reflection_task, synthesis_task],
|
| 441 |
+
verbose=True,
|
| 442 |
+
process=Process.sequential
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
# Execute the workflow
|
| 446 |
+
logger.info("Executing crew workflow")
|
| 447 |
+
result = crew.kickoff()
|
| 448 |
+
|
| 449 |
+
# Parse the result - convert CrewOutput to string first
|
| 450 |
+
result_str = str(result)
|
| 451 |
+
logger.info("Parsing analysis results")
|
| 452 |
+
parsed_result = self._parse_result(result_str)
|
| 453 |
+
|
| 454 |
+
# Add tool state information to the result
|
| 455 |
+
parsed_result["tool_states"] = self.tool_states
|
| 456 |
+
|
| 457 |
+
# Add analysis parameters
|
| 458 |
+
parsed_result["timeframe"] = self.timeframe
|
| 459 |
+
parsed_result["max_allocation"] = self.max_allocation
|
| 460 |
+
|
| 461 |
+
# Cap allocation at max_allocation if needed
|
| 462 |
+
if parsed_result.get("allocation_percentage", 0) > self.max_allocation:
|
| 463 |
+
logger.warning(f"Capping allocation from {parsed_result['allocation_percentage']}% to {self.max_allocation}%")
|
| 464 |
+
parsed_result["allocation_percentage"] = self.max_allocation
|
| 465 |
+
parsed_result["portfolio_allocation"] = self.max_allocation
|
| 466 |
+
|
| 467 |
+
# Log the summary of the result
|
| 468 |
+
logger.info(f"Analysis complete - Signal: {parsed_result['signal']}, Confidence: {parsed_result['confidence']}%, Allocation: {parsed_result.get('allocation_percentage', 0)}%")
|
| 469 |
+
|
| 470 |
+
# Check for tool errors and add a summary
|
| 471 |
+
tool_errors = []
|
| 472 |
+
for tool_name, state in self.tool_states.items():
|
| 473 |
+
if state.get("status") == "error" and state.get("error"):
|
| 474 |
+
tool_errors.append(f"{tool_name}: {state['error']}")
|
| 475 |
+
|
| 476 |
+
if tool_errors:
|
| 477 |
+
error_summary = "; ".join(tool_errors)
|
| 478 |
+
parsed_result["tool_error_summary"] = error_summary
|
| 479 |
+
logger.warning(f"Tool errors occurred during analysis: {error_summary}")
|
| 480 |
+
|
| 481 |
+
return parsed_result
|
| 482 |
+
|
| 483 |
+
except Exception as e:
|
| 484 |
+
logger.error(f"Error in Bitcoin analysis workflow: {str(e)}", exc_info=True)
|
| 485 |
+
return {
|
| 486 |
+
"error": str(e),
|
| 487 |
+
"signal": "hold", # Default to hold on error
|
| 488 |
+
"confidence": 0,
|
| 489 |
+
"portfolio_allocation": 0,
|
| 490 |
+
"allocation_percentage": 0,
|
| 491 |
+
"reasoning": f"Error in analysis: {str(e)}",
|
| 492 |
+
"tool_states": self.tool_states,
|
| 493 |
+
"timeframe": self.timeframe,
|
| 494 |
+
"max_allocation": self.max_allocation
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
def _parse_result(self, result: str) -> Dict[str, Any]:
|
| 498 |
+
"""
|
| 499 |
+
Parse the analysis result from text format
|
| 500 |
+
|
| 501 |
+
Args:
|
| 502 |
+
result: The text result from the crew
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
Structured dictionary with recommendation details
|
| 506 |
+
"""
|
| 507 |
+
# Default values
|
| 508 |
+
parsed = {
|
| 509 |
+
"signal": "hold",
|
| 510 |
+
"confidence": 0,
|
| 511 |
+
"allocation_percentage": 0,
|
| 512 |
+
"reasoning": "Analysis incomplete",
|
| 513 |
+
"raw_result": result
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
try:
|
| 517 |
+
logger.info("Parsing result from crew output")
|
| 518 |
+
|
| 519 |
+
# Extract recommendation (buy, sell, hold)
|
| 520 |
+
final_rec_match = re.search(r'final\s+recommendation:?\s*(buy|sell|hold)', result.lower())
|
| 521 |
+
if final_rec_match:
|
| 522 |
+
parsed["signal"] = final_rec_match.group(1).lower()
|
| 523 |
+
elif "buy" in result.lower():
|
| 524 |
+
parsed["signal"] = "buy"
|
| 525 |
+
elif "sell" in result.lower():
|
| 526 |
+
parsed["signal"] = "sell"
|
| 527 |
+
else:
|
| 528 |
+
parsed["signal"] = "hold"
|
| 529 |
+
|
| 530 |
+
# Extract confidence percentage using improved pattern matching
|
| 531 |
+
# Try multiple confidence patterns
|
| 532 |
+
confidence_patterns = [
|
| 533 |
+
r'final\s+confidence:?\s*(\d+)',
|
| 534 |
+
r'confidence:?\s*(\d+)',
|
| 535 |
+
r'confidence:?\s*(\d+)%',
|
| 536 |
+
r'with\s+(\d+)%\s+confidence'
|
| 537 |
+
]
|
| 538 |
+
|
| 539 |
+
for pattern in confidence_patterns:
|
| 540 |
+
confidence_match = re.search(pattern, result.lower())
|
| 541 |
+
if confidence_match:
|
| 542 |
+
parsed["confidence"] = int(confidence_match.group(1))
|
| 543 |
+
break
|
| 544 |
+
|
| 545 |
+
# Set minimum confidence for buy/sell signals
|
| 546 |
+
if parsed["signal"] in ["buy", "sell"] and parsed["confidence"] < 30:
|
| 547 |
+
parsed["confidence"] = 30
|
| 548 |
+
|
| 549 |
+
# Extract allocation percentage with improved patterns
|
| 550 |
+
allocation_patterns = [
|
| 551 |
+
r'allocation\s+percentage:?\s*(\d+)',
|
| 552 |
+
r'allocation:?\s*(\d+)',
|
| 553 |
+
r'allocation:?\s*(\d+)%',
|
| 554 |
+
r'allocate\s+(\d+)%'
|
| 555 |
+
]
|
| 556 |
+
|
| 557 |
+
for pattern in allocation_patterns:
|
| 558 |
+
allocation_match = re.search(pattern, result.lower())
|
| 559 |
+
if allocation_match:
|
| 560 |
+
parsed["allocation_percentage"] = int(allocation_match.group(1))
|
| 561 |
+
break
|
| 562 |
+
|
| 563 |
+
# Set minimum allocation for buy/sell signals
|
| 564 |
+
if parsed["signal"] in ["buy", "sell"] and parsed["allocation_percentage"] < 5:
|
| 565 |
+
parsed["allocation_percentage"] = 5
|
| 566 |
+
|
| 567 |
+
# Extract data reliability assessment if available
|
| 568 |
+
data_reliability_match = re.search(r'data\s+reliability:?\s*(.+?)(?:\n\n|\Z|technical\s+analysis)', result, re.IGNORECASE | re.DOTALL)
|
| 569 |
+
if data_reliability_match:
|
| 570 |
+
parsed["data_reliability"] = data_reliability_match.group(1).strip()
|
| 571 |
+
|
| 572 |
+
# Extract tool error assessment if available
|
| 573 |
+
tool_error_match = re.search(r'tool\s+error\s+assessment:?\s*(.+?)(?:\n\n|\Z|data\s+reliability)', result, re.IGNORECASE | re.DOTALL)
|
| 574 |
+
if tool_error_match:
|
| 575 |
+
parsed["tool_error_assessment"] = tool_error_match.group(1).strip()
|
| 576 |
+
|
| 577 |
+
# Extract reasoning if available - try to get the detailed reasoning section first
|
| 578 |
+
detailed_reasoning_match = re.search(r'detailed\s+reasoning:?\s*(.+?)(?:\n\n|\Z|market\s+outlook)', result, re.IGNORECASE | re.DOTALL)
|
| 579 |
+
if detailed_reasoning_match:
|
| 580 |
+
parsed["reasoning"] = detailed_reasoning_match.group(1).strip()
|
| 581 |
+
else:
|
| 582 |
+
reasoning_match = re.search(r'reasoning:?\s*(.+?)(?:\n\n|\Z)', result, re.IGNORECASE | re.DOTALL)
|
| 583 |
+
if reasoning_match:
|
| 584 |
+
parsed["reasoning"] = reasoning_match.group(1).strip()
|
| 585 |
+
else:
|
| 586 |
+
# Try to extract any paragraph that looks like reasoning
|
| 587 |
+
paragraphs = result.split('\n\n')
|
| 588 |
+
for paragraph in paragraphs:
|
| 589 |
+
if len(paragraph) > 100 and not paragraph.startswith('-'):
|
| 590 |
+
parsed["reasoning"] = paragraph.strip()
|
| 591 |
+
break
|
| 592 |
+
|
| 593 |
+
# Extract detailed analysis sections if available
|
| 594 |
+
tech_analysis_match = re.search(r'technical\s+analysis\s+summary:?\s*(.+?)(?:\n\n|\Z|initial\s+analysis)', result, re.IGNORECASE | re.DOTALL)
|
| 595 |
+
if tech_analysis_match:
|
| 596 |
+
parsed["technical_analysis"] = tech_analysis_match.group(1).strip()
|
| 597 |
+
|
| 598 |
+
initial_analysis_match = re.search(r'initial\s+analysis\s+summary:?\s*(.+?)(?:\n\n|\Z|reflection\s+analysis)', result, re.IGNORECASE | re.DOTALL)
|
| 599 |
+
if initial_analysis_match:
|
| 600 |
+
parsed["initial_analysis"] = initial_analysis_match.group(1).strip()
|
| 601 |
+
|
| 602 |
+
reflection_analysis_match = re.search(r'reflection\s+analysis\s+summary:?\s*(.+?)(?:\n\n|\Z|importance\s+weighting)', result, re.IGNORECASE | re.DOTALL)
|
| 603 |
+
if reflection_analysis_match:
|
| 604 |
+
parsed["reflection_analysis"] = reflection_analysis_match.group(1).strip()
|
| 605 |
+
|
| 606 |
+
impact_tech_match = re.search(r'impact\s+of\s+technical\s+factors:?\s*(.+?)(?:\n\n|\Z|impact\s+of\s+market)', result, re.IGNORECASE | re.DOTALL)
|
| 607 |
+
if impact_tech_match:
|
| 608 |
+
parsed["impact_technical"] = impact_tech_match.group(1).strip()
|
| 609 |
+
|
| 610 |
+
impact_market_match = re.search(r'impact\s+of\s+market\s+context:?\s*(.+?)(?:\n\n|\Z|impact\s+of\s+sentiment)', result, re.IGNORECASE | re.DOTALL)
|
| 611 |
+
if impact_market_match:
|
| 612 |
+
parsed["impact_market"] = impact_market_match.group(1).strip()
|
| 613 |
+
|
| 614 |
+
impact_sentiment_match = re.search(r'impact\s+of\s+sentiment:?\s*(.+?)(?:\n\n|\Z|final\s+recommendation)', result, re.IGNORECASE | re.DOTALL)
|
| 615 |
+
if impact_sentiment_match:
|
| 616 |
+
parsed["impact_sentiment"] = impact_sentiment_match.group(1).strip()
|
| 617 |
+
|
| 618 |
+
market_outlook_match = re.search(r'market\s+outlook:?\s*(.+?)(?:\n\n|\Z|risk\s+assessment)', result, re.IGNORECASE | re.DOTALL)
|
| 619 |
+
if market_outlook_match:
|
| 620 |
+
parsed["market_outlook"] = market_outlook_match.group(1).strip()
|
| 621 |
+
|
| 622 |
+
risk_assessment_match = re.search(r'risk\s+assessment:?\s*(.+?)(?:\n\n|\Z|trade\s+execution)', result, re.IGNORECASE | re.DOTALL)
|
| 623 |
+
if risk_assessment_match:
|
| 624 |
+
parsed["risk_assessment"] = risk_assessment_match.group(1).strip()
|
| 625 |
+
|
| 626 |
+
# Extract order execution details
|
| 627 |
+
# Look for a section that contains order execution details
|
| 628 |
+
execution_section_pattern = r'trade\s+execution:?\s*(.+?)(?:\n\n|\Z)'
|
| 629 |
+
execution_match = re.search(execution_section_pattern, result, re.IGNORECASE | re.DOTALL)
|
| 630 |
+
|
| 631 |
+
if execution_match:
|
| 632 |
+
execution_text = execution_match.group(1).strip()
|
| 633 |
+
parsed["order_execution_text"] = execution_text
|
| 634 |
+
|
| 635 |
+
# Try to extract order details if available
|
| 636 |
+
order_id_match = re.search(r'order\s+id:?\s*([a-f0-9-]+)', execution_text, re.IGNORECASE)
|
| 637 |
+
if order_id_match:
|
| 638 |
+
# This likely contains structured order info
|
| 639 |
+
parsed["order_execution"] = {
|
| 640 |
+
"order_id": order_id_match.group(1),
|
| 641 |
+
"symbol": re.search(r'symbol:?\s*([A-Z/]+)', execution_text, re.IGNORECASE).group(1) if re.search(r'symbol:?\s*([A-Z/]+)', execution_text, re.IGNORECASE) else "BTC/USD",
|
| 642 |
+
"side": re.search(r'side:?\s*(\w+)', execution_text, re.IGNORECASE).group(1) if re.search(r'side:?\s*(\w+)', execution_text, re.IGNORECASE) else parsed["signal"],
|
| 643 |
+
"quantity": re.search(r'quantity:?\s*([\d.]+)', execution_text, re.IGNORECASE).group(1) if re.search(r'quantity:?\s*([\d.]+)', execution_text, re.IGNORECASE) else "unknown",
|
| 644 |
+
"status": re.search(r'status:?\s*(\w+)', execution_text, re.IGNORECASE).group(1) if re.search(r'status:?\s*(\w+)', execution_text, re.IGNORECASE) else "unknown",
|
| 645 |
+
"success": True
|
| 646 |
+
}
|
| 647 |
+
elif "check" in execution_text.lower() and "account" in execution_text.lower():
|
| 648 |
+
# This is an account check (for hold signal)
|
| 649 |
+
# Extract any account details if available
|
| 650 |
+
cash_match = re.search(r'cash:?\s*([\d.]+)', execution_text, re.IGNORECASE)
|
| 651 |
+
equity_match = re.search(r'equity:?\s*([\d.]+)', execution_text, re.IGNORECASE)
|
| 652 |
+
|
| 653 |
+
account_info = "Account check completed"
|
| 654 |
+
if cash_match:
|
| 655 |
+
account_info += f", Cash: ${cash_match.group(1)}"
|
| 656 |
+
if equity_match:
|
| 657 |
+
account_info += f", Equity: ${equity_match.group(1)}"
|
| 658 |
+
|
| 659 |
+
parsed["order_execution"] = account_info
|
| 660 |
+
elif "failed" in execution_text.lower() or "error" in execution_text.lower():
|
| 661 |
+
# This is a failed order
|
| 662 |
+
error_message = "Unknown error"
|
| 663 |
+
error_match = re.search(r'error:?\s*(.+?)(?:\n|\Z)', execution_text, re.IGNORECASE)
|
| 664 |
+
if error_match:
|
| 665 |
+
error_message = error_match.group(1).strip()
|
| 666 |
+
|
| 667 |
+
parsed["order_execution"] = {
|
| 668 |
+
"success": False,
|
| 669 |
+
"error": error_message
|
| 670 |
+
}
|
| 671 |
+
else:
|
| 672 |
+
# Just include the raw text if we can't parse it
|
| 673 |
+
parsed["order_execution"] = execution_text
|
| 674 |
+
|
| 675 |
+
# Add portfolio allocation for backward compatibility
|
| 676 |
+
parsed["portfolio_allocation"] = parsed["allocation_percentage"]
|
| 677 |
+
|
| 678 |
+
logger.info(f"Successfully parsed result: signal={parsed['signal']}, confidence={parsed['confidence']}%, allocation={parsed['allocation_percentage']}%")
|
| 679 |
+
return parsed
|
| 680 |
+
|
| 681 |
+
except Exception as e:
|
| 682 |
+
logger.error(f"Error parsing result: {str(e)}", exc_info=True)
|
| 683 |
+
parsed["reasoning"] = f"Error parsing result: {str(e)}"
|
| 684 |
+
return parsed
|
| 685 |
+
|
| 686 |
+
def track_tool_call(self, tool_name: str, response: Dict[str, Any]) -> None:
|
| 687 |
+
"""
|
| 688 |
+
Track a tool call and update its state
|
| 689 |
+
|
| 690 |
+
Args:
|
| 691 |
+
tool_name: Name of the tool being called
|
| 692 |
+
response: Response from the tool call
|
| 693 |
+
"""
|
| 694 |
+
# Make sure the tool exists in our state tracking
|
| 695 |
+
if tool_name not in self.tool_states:
|
| 696 |
+
self.tool_states[tool_name] = {"status": "not_started", "error": None}
|
| 697 |
+
|
| 698 |
+
# Check if the response contains an error
|
| 699 |
+
if "error" in response:
|
| 700 |
+
self.tool_states[tool_name]["status"] = "error"
|
| 701 |
+
self.tool_states[tool_name]["error"] = response["error"]
|
| 702 |
+
logger.warning(f"Tool '{tool_name}' reported an error: {response['error']}")
|
| 703 |
+
else:
|
| 704 |
+
self.tool_states[tool_name]["status"] = "success"
|
| 705 |
+
self.tool_states[tool_name]["error"] = None
|
| 706 |
+
logger.info(f"Tool '{tool_name}' executed successfully")
|
| 707 |
+
|
| 708 |
+
# Monkey patch the tool _run methods to track errors
|
| 709 |
+
def _patch_tools_for_monitoring(self) -> None:
|
| 710 |
+
"""
|
| 711 |
+
Patch tool _run methods to track their execution status
|
| 712 |
+
"""
|
| 713 |
+
for tool_name, tool in [
|
| 714 |
+
("bitcoin_data", self.bitcoin_data_tool),
|
| 715 |
+
("bitcoin_news", self.bitcoin_news_tool),
|
| 716 |
+
("technical_strategy", self.technical_strategy_tool),
|
| 717 |
+
("crypto_market", self.crypto_market_tool),
|
| 718 |
+
("order_tool", self.order_tool)
|
| 719 |
+
]:
|
| 720 |
+
# Create and apply the wrapper immediately with the current values
|
| 721 |
+
# This ensures each closure gets its own copy of tool_name and original_run
|
| 722 |
+
def patch_tool(current_tool_name, current_tool):
|
| 723 |
+
original_run = current_tool._run
|
| 724 |
+
|
| 725 |
+
@functools.wraps(original_run)
|
| 726 |
+
def wrapped_run(*args, **kwargs):
|
| 727 |
+
try:
|
| 728 |
+
# Update tool state to running
|
| 729 |
+
self.tool_states[current_tool_name]["status"] = "running"
|
| 730 |
+
|
| 731 |
+
# Call the original function
|
| 732 |
+
result = original_run(*args, **kwargs)
|
| 733 |
+
|
| 734 |
+
# Track the result
|
| 735 |
+
self.track_tool_call(current_tool_name, result)
|
| 736 |
+
|
| 737 |
+
return result
|
| 738 |
+
except Exception as e:
|
| 739 |
+
# Update tool state to error
|
| 740 |
+
self.tool_states[current_tool_name]["status"] = "error"
|
| 741 |
+
self.tool_states[current_tool_name]["error"] = str(e)
|
| 742 |
+
logger.error(f"Exception in tool '{current_tool_name}': {str(e)}", exc_info=True)
|
| 743 |
+
|
| 744 |
+
# Return an error response
|
| 745 |
+
return {"error": f"Exception in tool execution: {str(e)}"}
|
| 746 |
+
|
| 747 |
+
# Apply the wrapped function to this tool
|
| 748 |
+
current_tool._run = wrapped_run
|
| 749 |
+
|
| 750 |
+
# Call the patching function with the current values
|
| 751 |
+
patch_tool(tool_name, tool)
|
src/crypto_analysis/main.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import traceback
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Dict, Any
|
| 8 |
+
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
from src.crypto_analysis.crew import BitcoinAnalysisCrew
|
| 13 |
+
|
| 14 |
+
# Set up logging
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=logging.INFO,
|
| 17 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 18 |
+
handlers=[
|
| 19 |
+
logging.StreamHandler(),
|
| 20 |
+
logging.FileHandler(os.path.join('logs', f'bitcoin_analysis_{datetime.now().strftime("%Y%m%d")}.log'))
|
| 21 |
+
]
|
| 22 |
+
)
|
| 23 |
+
logger = logging.getLogger("bitcoin_analysis")
|
| 24 |
+
|
| 25 |
+
# Ensure logs directory exists
|
| 26 |
+
os.makedirs('logs', exist_ok=True)
|
| 27 |
+
|
| 28 |
+
def run() -> Dict[str, Any]:
|
| 29 |
+
"""
|
| 30 |
+
Run the Bitcoin analysis crew and return the results
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with analysis results
|
| 34 |
+
"""
|
| 35 |
+
logger.info("Starting Bitcoin Price Sentiment Analysis")
|
| 36 |
+
print("## Starting Bitcoin Price Sentiment Analysis")
|
| 37 |
+
print("## " + "=" * 50)
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
# Create and run the Bitcoin analysis crew
|
| 41 |
+
bitcoin_crew = BitcoinAnalysisCrew()
|
| 42 |
+
result = bitcoin_crew.run_analysis()
|
| 43 |
+
|
| 44 |
+
# Check for errors
|
| 45 |
+
if "error" in result:
|
| 46 |
+
logger.error(f"Error in Bitcoin analysis: {result['error']}")
|
| 47 |
+
print(f"## ERROR: {result['error']}")
|
| 48 |
+
|
| 49 |
+
return result
|
| 50 |
+
except Exception as e:
|
| 51 |
+
error_traceback = traceback.format_exc()
|
| 52 |
+
logger.error(f"Unexpected error in run(): {str(e)}\n{error_traceback}")
|
| 53 |
+
return {
|
| 54 |
+
"error": str(e),
|
| 55 |
+
"traceback": error_traceback,
|
| 56 |
+
"signal": "hold", # Default to hold on error
|
| 57 |
+
"confidence": 0,
|
| 58 |
+
"portfolio_allocation": 0,
|
| 59 |
+
"reasoning": f"Unexpected error: {str(e)}"
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
def save_result(result: Dict[str, Any], output_dir: str = "results") -> str:
|
| 63 |
+
"""
|
| 64 |
+
Save analysis result to a JSON file
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
result: The analysis result to save
|
| 68 |
+
output_dir: Directory to save results in
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
Path to the saved file
|
| 72 |
+
"""
|
| 73 |
+
try:
|
| 74 |
+
# Create output directory if it doesn't exist
|
| 75 |
+
if not os.path.exists(output_dir):
|
| 76 |
+
os.makedirs(output_dir)
|
| 77 |
+
|
| 78 |
+
# Generate filename with timestamp
|
| 79 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 80 |
+
filename = f"bitcoin_analysis_{timestamp}.json"
|
| 81 |
+
filepath = os.path.join(output_dir, filename)
|
| 82 |
+
|
| 83 |
+
# Save result as JSON
|
| 84 |
+
with open(filepath, "w") as f:
|
| 85 |
+
json.dump(result, f, indent=2)
|
| 86 |
+
|
| 87 |
+
logger.info(f"Saved analysis result to {filepath}")
|
| 88 |
+
return filepath
|
| 89 |
+
except Exception as e:
|
| 90 |
+
error_traceback = traceback.format_exc()
|
| 91 |
+
logger.error(f"Error saving result: {str(e)}\n{error_traceback}")
|
| 92 |
+
|
| 93 |
+
# Try to save to a fallback location
|
| 94 |
+
try:
|
| 95 |
+
fallback_path = os.path.join(".", f"bitcoin_analysis_error_{timestamp}.json")
|
| 96 |
+
with open(fallback_path, "w") as f:
|
| 97 |
+
json.dump(result, f, indent=2)
|
| 98 |
+
logger.info(f"Saved analysis result to fallback location {fallback_path}")
|
| 99 |
+
return fallback_path
|
| 100 |
+
except:
|
| 101 |
+
logger.error("Failed to save result even to fallback location")
|
| 102 |
+
return "ERROR_SAVING_RESULT"
|
| 103 |
+
|
| 104 |
+
def format_output(result: Dict[str, Any]) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Format the analysis result as a readable string
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
result: The analysis result to format
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Formatted string representation
|
| 113 |
+
"""
|
| 114 |
+
signal = result.get("signal", "hold").upper()
|
| 115 |
+
confidence = result.get("confidence", 0)
|
| 116 |
+
allocation = result.get("portfolio_allocation", 0)
|
| 117 |
+
reasoning = result.get("reasoning", "No reasoning provided")
|
| 118 |
+
|
| 119 |
+
# Check for tool errors
|
| 120 |
+
tool_errors = result.get("tool_error_summary", "")
|
| 121 |
+
error_message = ""
|
| 122 |
+
if tool_errors:
|
| 123 |
+
error_message = f"## TOOL ERRORS DETECTED\n{tool_errors}\n\n"
|
| 124 |
+
|
| 125 |
+
# Check for data reliability information
|
| 126 |
+
data_reliability = result.get("data_reliability", "")
|
| 127 |
+
reliability_message = ""
|
| 128 |
+
if data_reliability:
|
| 129 |
+
reliability_message = f"## DATA RELIABILITY\n{data_reliability}\n\n"
|
| 130 |
+
|
| 131 |
+
output = [
|
| 132 |
+
"## BITCOIN TRADING RECOMMENDATION",
|
| 133 |
+
"## " + "=" * 50,
|
| 134 |
+
f"SIGNAL: {signal}",
|
| 135 |
+
f"CONFIDENCE: {confidence}%",
|
| 136 |
+
f"ALLOCATION: {allocation}% of portfolio",
|
| 137 |
+
""
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
if error_message:
|
| 141 |
+
output.append(error_message)
|
| 142 |
+
|
| 143 |
+
if reliability_message:
|
| 144 |
+
output.append(reliability_message)
|
| 145 |
+
|
| 146 |
+
output.extend([
|
| 147 |
+
"## REASONING",
|
| 148 |
+
reasoning
|
| 149 |
+
])
|
| 150 |
+
|
| 151 |
+
# Add market outlook if available
|
| 152 |
+
market_outlook = result.get("market_outlook", "")
|
| 153 |
+
if market_outlook:
|
| 154 |
+
output.extend([
|
| 155 |
+
"",
|
| 156 |
+
"## MARKET OUTLOOK",
|
| 157 |
+
market_outlook
|
| 158 |
+
])
|
| 159 |
+
|
| 160 |
+
# Add risk assessment if available
|
| 161 |
+
risk_assessment = result.get("risk_assessment", "")
|
| 162 |
+
if risk_assessment:
|
| 163 |
+
output.extend([
|
| 164 |
+
"",
|
| 165 |
+
"## RISK ASSESSMENT",
|
| 166 |
+
risk_assessment
|
| 167 |
+
])
|
| 168 |
+
|
| 169 |
+
# Add trade execution details if available
|
| 170 |
+
order_execution = result.get("order_execution_text", "")
|
| 171 |
+
if order_execution:
|
| 172 |
+
output.extend([
|
| 173 |
+
"",
|
| 174 |
+
"## TRADE EXECUTION",
|
| 175 |
+
order_execution
|
| 176 |
+
])
|
| 177 |
+
|
| 178 |
+
return "\n".join(output)
|
| 179 |
+
|
| 180 |
+
def monitor_mode():
|
| 181 |
+
"""
|
| 182 |
+
Run Bitcoin analysis in monitoring mode (continuous analysis at intervals)
|
| 183 |
+
"""
|
| 184 |
+
from time import sleep
|
| 185 |
+
|
| 186 |
+
logger.info("Starting Bitcoin Price Sentiment Analysis in Monitoring Mode")
|
| 187 |
+
print("## Starting Bitcoin Price Sentiment Analysis in Monitoring Mode")
|
| 188 |
+
print("## Analysis will run once every 4 hours")
|
| 189 |
+
print("## Press Ctrl+C to exit")
|
| 190 |
+
print("## " + "=" * 50)
|
| 191 |
+
|
| 192 |
+
interval_seconds = 4 * 60 * 60 # 4 hours
|
| 193 |
+
try:
|
| 194 |
+
run_count = 0
|
| 195 |
+
error_count = 0
|
| 196 |
+
max_consecutive_errors = 3
|
| 197 |
+
|
| 198 |
+
while True:
|
| 199 |
+
# Run analysis
|
| 200 |
+
start_time = datetime.now()
|
| 201 |
+
print(f"\n## Running analysis at {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 202 |
+
logger.info(f"Running analysis #{run_count + 1} at {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
result = run()
|
| 206 |
+
run_count += 1
|
| 207 |
+
|
| 208 |
+
# Check for errors
|
| 209 |
+
if "error" in result:
|
| 210 |
+
error_count += 1
|
| 211 |
+
print(f"## WARNING: Analysis completed with errors ({error_count}/{max_consecutive_errors})")
|
| 212 |
+
logger.warning(f"Analysis completed with errors: {result['error']}")
|
| 213 |
+
|
| 214 |
+
# If we have too many consecutive errors, increase sleep time to back off
|
| 215 |
+
if error_count >= max_consecutive_errors:
|
| 216 |
+
print(f"## Too many consecutive errors. Backing off...")
|
| 217 |
+
logger.warning(f"Too many consecutive errors ({error_count}). Backing off...")
|
| 218 |
+
interval_seconds = min(interval_seconds * 2, 12 * 60 * 60) # Max 12 hours
|
| 219 |
+
else:
|
| 220 |
+
# Reset error count if successful
|
| 221 |
+
error_count = 0
|
| 222 |
+
# Reset interval if it was increased
|
| 223 |
+
interval_seconds = 4 * 60 * 60
|
| 224 |
+
|
| 225 |
+
filepath = save_result(result)
|
| 226 |
+
|
| 227 |
+
print(format_output(result))
|
| 228 |
+
print(f"\n## Results saved to {filepath}")
|
| 229 |
+
|
| 230 |
+
# Calculate sleep time (accounting for analysis duration)
|
| 231 |
+
elapsed = (datetime.now() - start_time).total_seconds()
|
| 232 |
+
sleep_time = max(interval_seconds - elapsed, 0)
|
| 233 |
+
|
| 234 |
+
print(f"\n## Next analysis in {sleep_time/60/60:.2f} hours")
|
| 235 |
+
logger.info(f"Next analysis in {sleep_time/60/60:.2f} hours")
|
| 236 |
+
sleep(sleep_time)
|
| 237 |
+
|
| 238 |
+
except Exception as e:
|
| 239 |
+
error_traceback = traceback.format_exc()
|
| 240 |
+
error_count += 1
|
| 241 |
+
logger.error(f"Error in monitoring loop: {str(e)}\n{error_traceback}")
|
| 242 |
+
print(f"## ERROR in monitoring loop: {str(e)}")
|
| 243 |
+
|
| 244 |
+
# Save error information
|
| 245 |
+
error_result = {
|
| 246 |
+
"error": str(e),
|
| 247 |
+
"traceback": error_traceback,
|
| 248 |
+
"signal": "hold",
|
| 249 |
+
"confidence": 0,
|
| 250 |
+
"portfolio_allocation": 0,
|
| 251 |
+
"reasoning": f"Error in monitoring loop: {str(e)}"
|
| 252 |
+
}
|
| 253 |
+
save_result(error_result)
|
| 254 |
+
|
| 255 |
+
# If we have too many consecutive errors, increase sleep time
|
| 256 |
+
if error_count >= max_consecutive_errors:
|
| 257 |
+
print(f"## Too many consecutive errors ({error_count}/{max_consecutive_errors}). Backing off...")
|
| 258 |
+
logger.warning(f"Too many consecutive errors ({error_count}). Backing off...")
|
| 259 |
+
interval_seconds = min(interval_seconds * 2, 12 * 60 * 60) # Max 12 hours
|
| 260 |
+
|
| 261 |
+
# Sleep a shorter time before retrying
|
| 262 |
+
sleep_time = min(interval_seconds / 4, 60 * 60) # Min of 1/4 regular interval or 1 hour
|
| 263 |
+
print(f"## Retrying in {sleep_time/60:.0f} minutes...")
|
| 264 |
+
logger.info(f"Retrying in {sleep_time/60:.0f} minutes")
|
| 265 |
+
sleep(sleep_time)
|
| 266 |
+
|
| 267 |
+
except KeyboardInterrupt:
|
| 268 |
+
logger.info("Monitoring stopped by user")
|
| 269 |
+
print("\n## Monitoring stopped by user")
|
| 270 |
+
return
|
| 271 |
+
|
| 272 |
+
def train():
|
| 273 |
+
"""
|
| 274 |
+
Train the crew for a given number of iterations
|
| 275 |
+
"""
|
| 276 |
+
try:
|
| 277 |
+
iterations = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
| 278 |
+
logger.info(f"Training Bitcoin Analysis Crew for {iterations} iterations")
|
| 279 |
+
print(f"## Training Bitcoin Analysis Crew for {iterations} iterations")
|
| 280 |
+
|
| 281 |
+
bitcoin_crew = BitcoinAnalysisCrew()
|
| 282 |
+
bitcoin_crew.crew().train(n_iterations=iterations)
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
error_traceback = traceback.format_exc()
|
| 286 |
+
logger.error(f"Error training crew: {str(e)}\n{error_traceback}")
|
| 287 |
+
print(f"## Error training crew: {str(e)}")
|
| 288 |
+
|
| 289 |
+
if __name__ == "__main__":
|
| 290 |
+
try:
|
| 291 |
+
if len(sys.argv) > 1 and sys.argv[1] == "monitor":
|
| 292 |
+
# Run in monitoring mode
|
| 293 |
+
monitor_mode()
|
| 294 |
+
elif len(sys.argv) > 1 and sys.argv[1] == "train":
|
| 295 |
+
# Run in training mode
|
| 296 |
+
train()
|
| 297 |
+
else:
|
| 298 |
+
# Run once
|
| 299 |
+
result = run()
|
| 300 |
+
filepath = save_result(result)
|
| 301 |
+
|
| 302 |
+
print("\n\n" + "=" * 60)
|
| 303 |
+
print(format_output(result))
|
| 304 |
+
print("\n" + "=" * 60)
|
| 305 |
+
print(f"\nFull analysis result saved to: {filepath}")
|
| 306 |
+
except Exception as e:
|
| 307 |
+
error_traceback = traceback.format_exc()
|
| 308 |
+
logger.error(f"Unhandled exception in main: {str(e)}\n{error_traceback}")
|
| 309 |
+
print(f"## CRITICAL ERROR: {str(e)}")
|
| 310 |
+
print("See logs for details.")
|
src/crypto_analysis/test_tools.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script to verify that tools can be initialized properly.
|
| 3 |
+
This is helpful for checking API keys and connections.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
from src.crypto_analysis.tools import (
|
| 11 |
+
AlpacaBitcoinDataTool,
|
| 12 |
+
TechnicalIndicatorsTool,
|
| 13 |
+
BitcoinNewsTool,
|
| 14 |
+
BitcoinSentimentTool,
|
| 15 |
+
YahooBitcoinDataTool,
|
| 16 |
+
YahooCryptoMarketTool
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def test_alpaca_tools():
|
| 20 |
+
"""Test Alpaca tools initialization and basic functionality"""
|
| 21 |
+
print("Testing Alpaca Bitcoin Data Tool...")
|
| 22 |
+
|
| 23 |
+
# Check if API keys are set
|
| 24 |
+
api_key = os.getenv("ALPACA_API_KEY")
|
| 25 |
+
api_secret = os.getenv("ALPACA_API_SECRET")
|
| 26 |
+
|
| 27 |
+
if not api_key or not api_secret:
|
| 28 |
+
print("β οΈ Alpaca API keys not found in environment variables")
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# Initialize the tool
|
| 33 |
+
tool = AlpacaBitcoinDataTool()
|
| 34 |
+
print("β Tool initialized successfully")
|
| 35 |
+
|
| 36 |
+
# Test a simple data fetch
|
| 37 |
+
print("Fetching data (this might take a few seconds)...")
|
| 38 |
+
result = tool._run(timeframe="5Min", days_back=1)
|
| 39 |
+
|
| 40 |
+
if "error" in result:
|
| 41 |
+
print(f"β οΈ Error: {result['error']}")
|
| 42 |
+
else:
|
| 43 |
+
print(f"β Successfully retrieved {len(result.get('dataframe', []))} data points")
|
| 44 |
+
print(f"β Last Bitcoin price: ${result.get('last_price', 'N/A')}")
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 48 |
+
|
| 49 |
+
def test_technical_indicators():
|
| 50 |
+
"""Test technical indicators tool"""
|
| 51 |
+
print("\nTesting Technical Indicators Tool...")
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
# Initialize the tool
|
| 55 |
+
tool = TechnicalIndicatorsTool()
|
| 56 |
+
print("β Tool initialized successfully")
|
| 57 |
+
|
| 58 |
+
# Test indicators calculation
|
| 59 |
+
print("Calculating indicators (this might take a few seconds)...")
|
| 60 |
+
result = tool._run(timeframe="5Min", days_back=1)
|
| 61 |
+
|
| 62 |
+
if "error" in result:
|
| 63 |
+
print(f"β οΈ Error: {result['error']}")
|
| 64 |
+
else:
|
| 65 |
+
print(f"β Successfully calculated technical indicators")
|
| 66 |
+
print(f"β Current RSI: {result.get('indicators', {}).get('rsi', 'N/A')}")
|
| 67 |
+
print(f"β Current ADX: {result.get('indicators', {}).get('adx', 'N/A')}")
|
| 68 |
+
print(f"β Buy signal: {result.get('signals', {}).get('buy_signal', 'N/A')}")
|
| 69 |
+
print(f"β Sell signal: {result.get('signals', {}).get('sell_signal', 'N/A')}")
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 73 |
+
|
| 74 |
+
def test_news_tools():
|
| 75 |
+
"""Test news tools initialization and basic functionality using Tavily"""
|
| 76 |
+
print("\nTesting Bitcoin News Tool with Tavily...")
|
| 77 |
+
|
| 78 |
+
# Check if API key is set
|
| 79 |
+
api_key = os.getenv("TAVILY_API_KEY")
|
| 80 |
+
|
| 81 |
+
if not api_key:
|
| 82 |
+
print("β οΈ Tavily API key not found in environment variables")
|
| 83 |
+
print("Will try to fetch news using web search as fallback")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
# Initialize the tool
|
| 87 |
+
tool = BitcoinNewsTool()
|
| 88 |
+
print("β Tool initialized successfully")
|
| 89 |
+
|
| 90 |
+
# Test news fetch
|
| 91 |
+
print("Fetching news (this might take a few seconds)...")
|
| 92 |
+
result = tool._run(days_back=1, limit=3)
|
| 93 |
+
|
| 94 |
+
if "error" in result:
|
| 95 |
+
print(f"β οΈ Error: {result['error']}")
|
| 96 |
+
else:
|
| 97 |
+
articles = result.get("articles", [])
|
| 98 |
+
print(f"β Successfully retrieved {len(articles)} news articles")
|
| 99 |
+
|
| 100 |
+
# Print article titles
|
| 101 |
+
for i, article in enumerate(articles, 1):
|
| 102 |
+
print(f" {i}. {article.get('title', 'No title')}")
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 106 |
+
|
| 107 |
+
def test_yahoo_tools():
|
| 108 |
+
"""Test Yahoo Finance tools"""
|
| 109 |
+
print("\nTesting Yahoo Finance Bitcoin Data Tool...")
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
# Initialize the tool
|
| 113 |
+
tool = YahooBitcoinDataTool()
|
| 114 |
+
print("β Tool initialized successfully")
|
| 115 |
+
|
| 116 |
+
# Test data fetch
|
| 117 |
+
print("Fetching data (this might take a few seconds)...")
|
| 118 |
+
result = tool._run(period="1d", interval="1h")
|
| 119 |
+
|
| 120 |
+
if "error" in result:
|
| 121 |
+
print(f"β οΈ Error: {result['error']}")
|
| 122 |
+
else:
|
| 123 |
+
print(f"β Successfully retrieved Bitcoin data from Yahoo Finance")
|
| 124 |
+
print(f"β Last price: ${result.get('last_price', 'N/A')}")
|
| 125 |
+
|
| 126 |
+
# Print metadata
|
| 127 |
+
metadata = result.get("metadata", {})
|
| 128 |
+
print(f"β Market cap: {metadata.get('market_cap', 'N/A')}")
|
| 129 |
+
print(f"β 24h volume: {metadata.get('volume_24h', 'N/A')}")
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
print("=" * 50)
|
| 136 |
+
print("TESTING BITCOIN ANALYSIS TOOLS")
|
| 137 |
+
print("=" * 50)
|
| 138 |
+
|
| 139 |
+
# Test each tool category
|
| 140 |
+
test_yahoo_tools() # Start with Yahoo as it doesn't require API keys
|
| 141 |
+
test_alpaca_tools()
|
| 142 |
+
test_news_tools()
|
| 143 |
+
test_technical_indicators()
|
| 144 |
+
|
| 145 |
+
print("\n" + "=" * 50)
|
| 146 |
+
print("TESTING COMPLETE")
|
| 147 |
+
print("=" * 50)
|
src/crypto_analysis/tools/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crypto analysis tools for Bitcoin price and sentiment analysis.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# Import and expose tools
|
| 6 |
+
from .bitcoin_tools import (
|
| 7 |
+
YahooBitcoinDataTool,
|
| 8 |
+
RealBitcoinNewsTool
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
from .technical_tools import (
|
| 12 |
+
TechnicalAnalysisStrategy
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
from .order_tools import AlpacaCryptoOrderTool
|
| 16 |
+
|
| 17 |
+
from .yahoo_tools import YahooCryptoMarketTool
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
'YahooBitcoinDataTool',
|
| 21 |
+
'RealBitcoinNewsTool',
|
| 22 |
+
'TechnicalAnalysisStrategy',
|
| 23 |
+
'AlpacaCryptoOrderTool',
|
| 24 |
+
'YahooCryptoMarketTool'
|
| 25 |
+
]
|
src/crypto_analysis/tools/alpaca_tools.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import os
|
| 2 |
+
# from typing import Dict, Any, List, Optional
|
| 3 |
+
# from datetime import datetime, timedelta
|
| 4 |
+
# import pandas as pd
|
| 5 |
+
# import pandas_ta as ta
|
| 6 |
+
# from crewai_tools.tools import BaseTool
|
| 7 |
+
# from alpaca.data.historical import CryptoHistoricalDataClient
|
| 8 |
+
# from alpaca.data.requests import CryptoBarsRequest
|
| 9 |
+
# from alpaca.data.timeframe import TimeFrame
|
| 10 |
+
|
| 11 |
+
# class AlpacaBitcoinDataTool(BaseTool):
|
| 12 |
+
# name: str = "Bitcoin Price Data Tool"
|
| 13 |
+
# description: str = "Fetches Bitcoin price data including OHLCV (Open, High, Low, Close, Volume) for technical analysis"
|
| 14 |
+
|
| 15 |
+
# def __init__(self):
|
| 16 |
+
# super().__init__()
|
| 17 |
+
# self.api_key = os.getenv("ALPACA_API_KEY")
|
| 18 |
+
# self.api_secret = os.getenv("ALPACA_API_SECRET")
|
| 19 |
+
# self.client = CryptoHistoricalDataClient(api_key=self.api_key, secret_key=self.api_secret)
|
| 20 |
+
|
| 21 |
+
# def _run(self, timeframe: str = "5Min", days_back: int = 30) -> Dict[str, Any]:
|
| 22 |
+
# """
|
| 23 |
+
# Fetch Bitcoin price data from Alpaca
|
| 24 |
+
|
| 25 |
+
# Args:
|
| 26 |
+
# timeframe: Time interval (1Min, 5Min, 15Min, 1Hour, 1Day)
|
| 27 |
+
# days_back: Number of days of historical data to fetch
|
| 28 |
+
|
| 29 |
+
# Returns:
|
| 30 |
+
# Dictionary with OHLCV data and dataframe
|
| 31 |
+
# """
|
| 32 |
+
# # Map string timeframe to TimeFrame enum
|
| 33 |
+
# timeframe_map = {
|
| 34 |
+
# "1Min": TimeFrame.MINUTE,
|
| 35 |
+
# "5Min": TimeFrame.MINUTE_5,
|
| 36 |
+
# "15Min": TimeFrame.MINUTE_15,
|
| 37 |
+
# "1Hour": TimeFrame.HOUR,
|
| 38 |
+
# "1Day": TimeFrame.DAY
|
| 39 |
+
# }
|
| 40 |
+
|
| 41 |
+
# tf = timeframe_map.get(timeframe, TimeFrame.MINUTE_5)
|
| 42 |
+
|
| 43 |
+
# # Calculate start and end dates
|
| 44 |
+
# end = datetime.now()
|
| 45 |
+
# start = end - timedelta(days=days_back)
|
| 46 |
+
|
| 47 |
+
# # Create the request
|
| 48 |
+
# request_params = CryptoBarsRequest(
|
| 49 |
+
# symbol_or_symbols=["BTCUSD"],
|
| 50 |
+
# timeframe=tf,
|
| 51 |
+
# start=start,
|
| 52 |
+
# end=end
|
| 53 |
+
# )
|
| 54 |
+
|
| 55 |
+
# try:
|
| 56 |
+
# # Get the data
|
| 57 |
+
# bars = self.client.get_crypto_bars(request_params)
|
| 58 |
+
|
| 59 |
+
# # Convert to dataframe
|
| 60 |
+
# df = bars.df.reset_index()
|
| 61 |
+
|
| 62 |
+
# # Ensure proper column names
|
| 63 |
+
# if 'timestamp' in df.columns:
|
| 64 |
+
# df = df.rename(columns={'timestamp': 'time'})
|
| 65 |
+
|
| 66 |
+
# # Select only BTCUSD data and reset index for simpler access
|
| 67 |
+
# if 'symbol' in df.columns:
|
| 68 |
+
# df = df[df['symbol'] == 'BTCUSD'].reset_index(drop=True)
|
| 69 |
+
# df = df.drop(columns=['symbol'])
|
| 70 |
+
|
| 71 |
+
# # Convert to dictionary for JSON serialization
|
| 72 |
+
# data_dict = {
|
| 73 |
+
# "dataframe": df.to_dict(orient='records'),
|
| 74 |
+
# "last_price": float(df['close'].iloc[-1]),
|
| 75 |
+
# "time_period": timeframe,
|
| 76 |
+
# "days_back": days_back,
|
| 77 |
+
# "start_date": start.isoformat(),
|
| 78 |
+
# "end_date": end.isoformat()
|
| 79 |
+
# }
|
| 80 |
+
|
| 81 |
+
# return data_dict
|
| 82 |
+
|
| 83 |
+
# except Exception as e:
|
| 84 |
+
# return {"error": str(e)}
|
| 85 |
+
|
| 86 |
+
# class TechnicalIndicatorsTool(BaseTool):
|
| 87 |
+
# name: str = "Bitcoin Technical Indicators Tool"
|
| 88 |
+
# description: str = "Calculates technical indicators for Bitcoin price data such as RSI, MACD, Bollinger Bands, and ADX"
|
| 89 |
+
|
| 90 |
+
# def __init__(self):
|
| 91 |
+
# super().__init__()
|
| 92 |
+
# self.alpaca_tool = AlpacaBitcoinDataTool()
|
| 93 |
+
|
| 94 |
+
# def _run(self, timeframe: str = "5Min", days_back: int = 30) -> Dict[str, Any]:
|
| 95 |
+
# """
|
| 96 |
+
# Calculate technical indicators for Bitcoin
|
| 97 |
+
|
| 98 |
+
# Args:
|
| 99 |
+
# timeframe: Time interval (1Min, 5Min, 15Min, 1Hour, 1Day)
|
| 100 |
+
# days_back: Number of days of historical data to fetch
|
| 101 |
+
|
| 102 |
+
# Returns:
|
| 103 |
+
# Dictionary with technical indicators and signals
|
| 104 |
+
# """
|
| 105 |
+
# # Get price data
|
| 106 |
+
# price_data = self.alpaca_tool._run(timeframe=timeframe, days_back=days_back)
|
| 107 |
+
|
| 108 |
+
# if "error" in price_data:
|
| 109 |
+
# return price_data
|
| 110 |
+
|
| 111 |
+
# # Convert back to dataframe
|
| 112 |
+
# df = pd.DataFrame(price_data["dataframe"])
|
| 113 |
+
|
| 114 |
+
# # Calculate indicators
|
| 115 |
+
# # RSI (Relative Strength Index)
|
| 116 |
+
# df['rsi'] = ta.rsi(df['close'], length=14)
|
| 117 |
+
|
| 118 |
+
# # Bollinger Bands
|
| 119 |
+
# bollinger = ta.bbands(df['close'], length=20, std=2)
|
| 120 |
+
# df = pd.concat([df, bollinger], axis=1)
|
| 121 |
+
|
| 122 |
+
# # ADX (Average Directional Index)
|
| 123 |
+
# adx = ta.adx(df['high'], df['low'], df['close'], length=14)
|
| 124 |
+
# df = pd.concat([df, adx], axis=1)
|
| 125 |
+
|
| 126 |
+
# # MACD (Moving Average Convergence Divergence)
|
| 127 |
+
# macd = ta.macd(df['close'])
|
| 128 |
+
# df = pd.concat([df, macd], axis=1)
|
| 129 |
+
|
| 130 |
+
# # Get the most recent values for analysis
|
| 131 |
+
# current_rsi = df['rsi'].iloc[-1]
|
| 132 |
+
# current_adx = df['ADX_14'].iloc[-1] if 'ADX_14' in df.columns else None
|
| 133 |
+
# current_price = df['close'].iloc[-1]
|
| 134 |
+
# upper_band = df['BBU_20_2.0'].iloc[-1] if 'BBU_20_2.0' in df.columns else None
|
| 135 |
+
# lower_band = df['BBL_20_2.0'].iloc[-1] if 'BBL_20_2.0' in df.columns else None
|
| 136 |
+
|
| 137 |
+
# # Previous price (for checking if price crossed Bollinger Bands)
|
| 138 |
+
# prev_price = df['close'].iloc[-2]
|
| 139 |
+
# prev_lower_band = df['BBL_20_2.0'].iloc[-2] if 'BBL_20_2.0' in df.columns else None
|
| 140 |
+
# prev_upper_band = df['BBU_20_2.0'].iloc[-2] if 'BBU_20_2.0' in df.columns else None
|
| 141 |
+
|
| 142 |
+
# # Check for buy signal (based on user's strategy)
|
| 143 |
+
# buy_signal = (
|
| 144 |
+
# (current_adx is not None and current_adx < 25) and
|
| 145 |
+
# (current_rsi is not None and current_rsi < 30) and
|
| 146 |
+
# (lower_band is not None and prev_lower_band is not None) and
|
| 147 |
+
# (prev_price < prev_lower_band and current_price > lower_band)
|
| 148 |
+
# )
|
| 149 |
+
|
| 150 |
+
# # Check for sell signal (based on user's strategy)
|
| 151 |
+
# sell_signal = (
|
| 152 |
+
# (current_adx is not None and current_adx < 25) and
|
| 153 |
+
# (current_rsi is not None and current_rsi > 70) and
|
| 154 |
+
# (upper_band is not None and prev_upper_band is not None) and
|
| 155 |
+
# (prev_price > prev_upper_band and current_price < upper_band)
|
| 156 |
+
# )
|
| 157 |
+
|
| 158 |
+
# # Return all data and signals
|
| 159 |
+
# result = {
|
| 160 |
+
# "time": df['time'].iloc[-1],
|
| 161 |
+
# "close_price": float(current_price),
|
| 162 |
+
# "indicators": {
|
| 163 |
+
# "rsi": float(current_rsi) if not pd.isna(current_rsi) else None,
|
| 164 |
+
# "adx": float(current_adx) if current_adx is not None and not pd.isna(current_adx) else None,
|
| 165 |
+
# "upper_band": float(upper_band) if upper_band is not None and not pd.isna(upper_band) else None,
|
| 166 |
+
# "middle_band": float(df['BBM_20_2.0'].iloc[-1]) if 'BBM_20_2.0' in df.columns and not pd.isna(df['BBM_20_2.0'].iloc[-1]) else None,
|
| 167 |
+
# "lower_band": float(lower_band) if lower_band is not None and not pd.isna(lower_band) else None,
|
| 168 |
+
# "macd": float(df['MACD_12_26_9'].iloc[-1]) if 'MACD_12_26_9' in df.columns and not pd.isna(df['MACD_12_26_9'].iloc[-1]) else None,
|
| 169 |
+
# "macd_signal": float(df['MACDs_12_26_9'].iloc[-1]) if 'MACDs_12_26_9' in df.columns and not pd.isna(df['MACDs_12_26_9'].iloc[-1]) else None
|
| 170 |
+
# },
|
| 171 |
+
# "signals": {
|
| 172 |
+
# "buy_signal": bool(buy_signal),
|
| 173 |
+
# "sell_signal": bool(sell_signal),
|
| 174 |
+
# "market_condition": "ranging" if current_adx < 25 else "trending"
|
| 175 |
+
# },
|
| 176 |
+
# "dataframe": df.tail(10).to_dict(orient='records') # Last 10 records
|
| 177 |
+
# }
|
| 178 |
+
|
| 179 |
+
# return result
|
src/crypto_analysis/tools/bitcoin_tools.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Bitcoin analysis tools for CrewAI agents - Simplified Version
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from typing import Dict, Any, List, Optional, ClassVar
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import yfinance as yf
|
| 10 |
+
from crewai.tools import BaseTool
|
| 11 |
+
from pydantic import Field
|
| 12 |
+
import requests
|
| 13 |
+
from bs4 import BeautifulSoup
|
| 14 |
+
import time
|
| 15 |
+
import random
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
os.environ["SERPER_API_KEY"] = "your_serper_api_key"
|
| 19 |
+
|
| 20 |
+
class YahooBitcoinDataTool(BaseTool):
|
| 21 |
+
"""Tool for fetching Bitcoin data from Yahoo Finance"""
|
| 22 |
+
name: str = "Bitcoin Price Data Tool"
|
| 23 |
+
description: str = "Get the latest Bitcoin price data from Yahoo Finance"
|
| 24 |
+
|
| 25 |
+
def _run(self) -> Dict[str, Any]:
|
| 26 |
+
"""
|
| 27 |
+
Fetch latest Bitcoin data from Yahoo Finance
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Dictionary with Bitcoin price data
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
# Get Bitcoin data from Yahoo Finance
|
| 34 |
+
btc_data = yf.Ticker("BTC-USD")
|
| 35 |
+
history = btc_data.history(period="1d")
|
| 36 |
+
|
| 37 |
+
if history.empty:
|
| 38 |
+
return {
|
| 39 |
+
"error": "No data available",
|
| 40 |
+
"price": 0,
|
| 41 |
+
"market_cap": 0,
|
| 42 |
+
"percent_change": 0,
|
| 43 |
+
"trend": "unknown"
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# Extract latest price data
|
| 47 |
+
latest_price = history['Close'].iloc[-1]
|
| 48 |
+
|
| 49 |
+
# Get market cap (Yahoo provides this)
|
| 50 |
+
info = btc_data.info
|
| 51 |
+
market_cap = info.get('marketCap', 0)
|
| 52 |
+
|
| 53 |
+
# Calculate percent change
|
| 54 |
+
if len(history) > 1:
|
| 55 |
+
prev_close = history['Close'].iloc[-2]
|
| 56 |
+
percent_change = ((latest_price - prev_close) / prev_close) * 100
|
| 57 |
+
else:
|
| 58 |
+
percent_change = 0
|
| 59 |
+
|
| 60 |
+
# Determine trend (simple version)
|
| 61 |
+
trend = "bullish" if percent_change > 0 else "bearish"
|
| 62 |
+
|
| 63 |
+
return {
|
| 64 |
+
"price": round(latest_price, 2),
|
| 65 |
+
"market_cap": market_cap,
|
| 66 |
+
"percent_change": round(percent_change, 2),
|
| 67 |
+
"trend": trend
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
return {
|
| 72 |
+
"error": str(e),
|
| 73 |
+
"price": 0,
|
| 74 |
+
"market_cap": 0,
|
| 75 |
+
"percent_change": 0,
|
| 76 |
+
"trend": "unknown"
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
class RealBitcoinNewsTool(BaseTool):
|
| 80 |
+
"""Tool for fetching actual Bitcoin news from the web using direct HTTP requests"""
|
| 81 |
+
name: str = "Bitcoin News Tool"
|
| 82 |
+
description: str = "Fetches the latest Bitcoin news and analysis from financial news sources"
|
| 83 |
+
|
| 84 |
+
# Class variables need to be annotated with ClassVar
|
| 85 |
+
USER_AGENTS: ClassVar[List[str]] = [
|
| 86 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
| 87 |
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
|
| 88 |
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
# Define common crypto news sources
|
| 92 |
+
NEWS_SOURCES: ClassVar[Dict[str, str]] = {
|
| 93 |
+
'coindesk': 'https://www.coindesk.com/tag/bitcoin/',
|
| 94 |
+
'cointelegraph': 'https://cointelegraph.com/tags/bitcoin',
|
| 95 |
+
'decrypt': 'https://decrypt.co/categories/bitcoin',
|
| 96 |
+
'bitcoinmagazine': 'https://bitcoinmagazine.com/',
|
| 97 |
+
'google_news': 'https://news.google.com/search?q=bitcoin&hl=en-US'
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
def _run(self, source: str = None, count: int = 5) -> Dict[str, Any]:
|
| 101 |
+
"""
|
| 102 |
+
Fetch Bitcoin news directly from selected news sources
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
source: Optional specific source to check (e.g., "coindesk", "cointelegraph", "google_news")
|
| 106 |
+
count: Maximum number of articles to retrieve
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Dictionary with Bitcoin news articles
|
| 110 |
+
"""
|
| 111 |
+
articles = []
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
# If source is specified, only use that source
|
| 115 |
+
sources_to_check = {source: self.NEWS_SOURCES[source]} if source and source in self.NEWS_SOURCES else self.NEWS_SOURCES
|
| 116 |
+
|
| 117 |
+
# Try each source until we get enough articles
|
| 118 |
+
for src_name, url in list(sources_to_check.items())[:3]: # Limit to 3 sources to avoid too many requests
|
| 119 |
+
if len(articles) >= count:
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
# Get news from this source
|
| 124 |
+
source_articles = self._fetch_from_source(src_name, url, count - len(articles))
|
| 125 |
+
articles.extend(source_articles)
|
| 126 |
+
|
| 127 |
+
# Add a small delay between requests to be nice to servers
|
| 128 |
+
time.sleep(1)
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
print(f"Error fetching from {src_name}: {e}")
|
| 132 |
+
|
| 133 |
+
# If we couldn't get any articles, try searching Google News
|
| 134 |
+
if not articles and 'google_news' in self.NEWS_SOURCES:
|
| 135 |
+
try:
|
| 136 |
+
google_articles = self._fetch_from_source('google_news', self.NEWS_SOURCES['google_news'], count)
|
| 137 |
+
articles.extend(google_articles)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Error fetching from Google News: {e}")
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"General error fetching news: {e}")
|
| 143 |
+
|
| 144 |
+
# If we still couldn't get any real data, use fallback
|
| 145 |
+
if not articles:
|
| 146 |
+
return self._get_fallback_data()
|
| 147 |
+
|
| 148 |
+
# Return structured result
|
| 149 |
+
return {
|
| 150 |
+
"articles": articles,
|
| 151 |
+
"count": len(articles),
|
| 152 |
+
"period": "Latest available data",
|
| 153 |
+
"timestamp": datetime.now().isoformat()
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
def _fetch_from_source(self, source_name: str, url: str, count: int) -> List[Dict[str, Any]]:
|
| 157 |
+
"""Extract articles from a specific news source"""
|
| 158 |
+
articles = []
|
| 159 |
+
headers = {'User-Agent': random.choice(self.USER_AGENTS)}
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 163 |
+
response.raise_for_status()
|
| 164 |
+
|
| 165 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 166 |
+
|
| 167 |
+
# Different parsing logic for different sites
|
| 168 |
+
if source_name == 'coindesk':
|
| 169 |
+
articles = self._parse_coindesk(soup, count)
|
| 170 |
+
elif source_name == 'cointelegraph':
|
| 171 |
+
articles = self._parse_cointelegraph(soup, count)
|
| 172 |
+
elif source_name == 'decrypt':
|
| 173 |
+
articles = self._parse_decrypt(soup, count)
|
| 174 |
+
elif source_name == 'bitcoinmagazine':
|
| 175 |
+
articles = self._parse_bitcoinmagazine(soup, count)
|
| 176 |
+
elif source_name == 'google_news':
|
| 177 |
+
articles = self._parse_google_news(soup, count)
|
| 178 |
+
|
| 179 |
+
# If we extracted some articles but the parser didn't add source or date
|
| 180 |
+
for article in articles:
|
| 181 |
+
if 'source' not in article or not article['source']:
|
| 182 |
+
article['source'] = source_name.title()
|
| 183 |
+
if 'published_at' not in article or not article['published_at']:
|
| 184 |
+
article['published_at'] = datetime.now().isoformat()
|
| 185 |
+
|
| 186 |
+
return articles[:count] # Limit to requested count
|
| 187 |
+
|
| 188 |
+
except Exception as e:
|
| 189 |
+
print(f"Error in _fetch_from_source for {source_name}: {e}")
|
| 190 |
+
return []
|
| 191 |
+
|
| 192 |
+
def _parse_coindesk(self, soup: BeautifulSoup, count: int) -> List[Dict[str, Any]]:
|
| 193 |
+
"""Parse CoinDesk articles"""
|
| 194 |
+
articles = []
|
| 195 |
+
try:
|
| 196 |
+
# Find article elements (adjust selectors based on actual HTML structure)
|
| 197 |
+
article_elements = soup.select('article') or soup.select('.article-card')
|
| 198 |
+
|
| 199 |
+
for element in article_elements[:count]:
|
| 200 |
+
title_elem = element.select_one('h2, h3, .heading') or element
|
| 201 |
+
link_elem = element.select_one('a[href]')
|
| 202 |
+
desc_elem = element.select_one('p, .description') or title_elem
|
| 203 |
+
|
| 204 |
+
title = title_elem.get_text().strip() if title_elem else "Bitcoin News"
|
| 205 |
+
description = desc_elem.get_text().strip() if desc_elem else ""
|
| 206 |
+
url = link_elem.get('href') if link_elem else None
|
| 207 |
+
|
| 208 |
+
# Make URL absolute if it's relative
|
| 209 |
+
if url and not url.startswith('http'):
|
| 210 |
+
url = f"https://www.coindesk.com{url}"
|
| 211 |
+
|
| 212 |
+
if title: # Only add if we at least have a title
|
| 213 |
+
articles.append({
|
| 214 |
+
'title': title,
|
| 215 |
+
'description': description or "Recent Bitcoin news from CoinDesk",
|
| 216 |
+
'source': 'CoinDesk',
|
| 217 |
+
'url': url,
|
| 218 |
+
'published_at': datetime.now().isoformat()
|
| 219 |
+
})
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Error parsing CoinDesk: {e}")
|
| 222 |
+
|
| 223 |
+
return articles
|
| 224 |
+
|
| 225 |
+
def _parse_cointelegraph(self, soup: BeautifulSoup, count: int) -> List[Dict[str, Any]]:
|
| 226 |
+
"""Parse CoinTelegraph articles"""
|
| 227 |
+
articles = []
|
| 228 |
+
try:
|
| 229 |
+
# Find article elements (adjust selectors based on actual HTML structure)
|
| 230 |
+
article_elements = soup.select('.post-card') or soup.select('article')
|
| 231 |
+
|
| 232 |
+
for element in article_elements[:count]:
|
| 233 |
+
title_elem = element.select_one('h2') or element.select_one('.post-card__title')
|
| 234 |
+
link_elem = element.select_one('a[href]')
|
| 235 |
+
desc_elem = element.select_one('p') or element.select_one('.post-card__text')
|
| 236 |
+
|
| 237 |
+
title = title_elem.get_text().strip() if title_elem else "Bitcoin News"
|
| 238 |
+
description = desc_elem.get_text().strip() if desc_elem else ""
|
| 239 |
+
url = link_elem.get('href') if link_elem else None
|
| 240 |
+
|
| 241 |
+
# Make URL absolute if it's relative
|
| 242 |
+
if url and not url.startswith('http'):
|
| 243 |
+
url = f"https://cointelegraph.com{url}"
|
| 244 |
+
|
| 245 |
+
if title: # Only add if we at least have a title
|
| 246 |
+
articles.append({
|
| 247 |
+
'title': title,
|
| 248 |
+
'description': description or "Recent Bitcoin news from CoinTelegraph",
|
| 249 |
+
'source': 'CoinTelegraph',
|
| 250 |
+
'url': url,
|
| 251 |
+
'published_at': datetime.now().isoformat()
|
| 252 |
+
})
|
| 253 |
+
except Exception as e:
|
| 254 |
+
print(f"Error parsing CoinTelegraph: {e}")
|
| 255 |
+
|
| 256 |
+
return articles
|
| 257 |
+
|
| 258 |
+
def _parse_decrypt(self, soup: BeautifulSoup, count: int) -> List[Dict[str, Any]]:
|
| 259 |
+
"""Parse Decrypt articles"""
|
| 260 |
+
articles = []
|
| 261 |
+
try:
|
| 262 |
+
# Find article elements (adjust selectors based on actual HTML structure)
|
| 263 |
+
article_elements = soup.select('.card') or soup.select('article')
|
| 264 |
+
|
| 265 |
+
for element in article_elements[:count]:
|
| 266 |
+
title_elem = element.select_one('h3') or element.select_one('.title')
|
| 267 |
+
link_elem = element.select_one('a[href]')
|
| 268 |
+
desc_elem = element.select_one('p') or element.select_one('.excerpt')
|
| 269 |
+
|
| 270 |
+
title = title_elem.get_text().strip() if title_elem else "Bitcoin News"
|
| 271 |
+
description = desc_elem.get_text().strip() if desc_elem else ""
|
| 272 |
+
url = link_elem.get('href') if link_elem else None
|
| 273 |
+
|
| 274 |
+
if title: # Only add if we at least have a title
|
| 275 |
+
articles.append({
|
| 276 |
+
'title': title,
|
| 277 |
+
'description': description or "Recent Bitcoin news from Decrypt",
|
| 278 |
+
'source': 'Decrypt',
|
| 279 |
+
'url': url,
|
| 280 |
+
'published_at': datetime.now().isoformat()
|
| 281 |
+
})
|
| 282 |
+
except Exception as e:
|
| 283 |
+
print(f"Error parsing Decrypt: {e}")
|
| 284 |
+
|
| 285 |
+
return articles
|
| 286 |
+
|
| 287 |
+
def _parse_bitcoinmagazine(self, soup: BeautifulSoup, count: int) -> List[Dict[str, Any]]:
|
| 288 |
+
"""Parse BitcoinMagazine articles"""
|
| 289 |
+
articles = []
|
| 290 |
+
try:
|
| 291 |
+
# Find article elements (adjust selectors based on actual HTML structure)
|
| 292 |
+
article_elements = soup.select('.article') or soup.select('article') or soup.select('.post')
|
| 293 |
+
|
| 294 |
+
for element in article_elements[:count]:
|
| 295 |
+
title_elem = element.select_one('h2, h3') or element.select_one('.title')
|
| 296 |
+
link_elem = element.select_one('a[href]')
|
| 297 |
+
desc_elem = element.select_one('p') or element.select_one('.excerpt, .summary')
|
| 298 |
+
|
| 299 |
+
title = title_elem.get_text().strip() if title_elem else "Bitcoin News"
|
| 300 |
+
description = desc_elem.get_text().strip() if desc_elem else ""
|
| 301 |
+
url = link_elem.get('href') if link_elem else None
|
| 302 |
+
|
| 303 |
+
if title: # Only add if we at least have a title
|
| 304 |
+
articles.append({
|
| 305 |
+
'title': title,
|
| 306 |
+
'description': description or "Recent Bitcoin news from Bitcoin Magazine",
|
| 307 |
+
'source': 'Bitcoin Magazine',
|
| 308 |
+
'url': url,
|
| 309 |
+
'published_at': datetime.now().isoformat()
|
| 310 |
+
})
|
| 311 |
+
except Exception as e:
|
| 312 |
+
print(f"Error parsing Bitcoin Magazine: {e}")
|
| 313 |
+
|
| 314 |
+
return articles
|
| 315 |
+
|
| 316 |
+
def _parse_google_news(self, soup: BeautifulSoup, count: int) -> List[Dict[str, Any]]:
|
| 317 |
+
"""Parse Google News search results"""
|
| 318 |
+
articles = []
|
| 319 |
+
try:
|
| 320 |
+
# Find article elements (adjust selectors based on actual HTML structure)
|
| 321 |
+
article_elements = soup.select('article') or soup.select('.xrnccd')
|
| 322 |
+
|
| 323 |
+
for element in article_elements[:count]:
|
| 324 |
+
title_elem = element.select_one('h3, h4') or element.select_one('.DY5T1d')
|
| 325 |
+
source_elem = element.select_one('.wEwyrc') or element.select_one('.SVJrMe')
|
| 326 |
+
time_elem = element.select_one('time') or element.select_one('.WW6dff')
|
| 327 |
+
link_elem = element.select_one('a[href]')
|
| 328 |
+
|
| 329 |
+
title = title_elem.get_text().strip() if title_elem else "Bitcoin News"
|
| 330 |
+
source = source_elem.get_text().strip() if source_elem else "Google News"
|
| 331 |
+
|
| 332 |
+
# Try to extract link - Google News has complex link structure
|
| 333 |
+
url = None
|
| 334 |
+
if link_elem:
|
| 335 |
+
url = link_elem.get('href')
|
| 336 |
+
if url and url.startswith('./articles/'):
|
| 337 |
+
url = f"https://news.google.com{url[1:]}"
|
| 338 |
+
|
| 339 |
+
if title: # Only add if we at least have a title
|
| 340 |
+
articles.append({
|
| 341 |
+
'title': title,
|
| 342 |
+
'description': f"Bitcoin news from {source}",
|
| 343 |
+
'source': source,
|
| 344 |
+
'url': url,
|
| 345 |
+
'published_at': datetime.now().isoformat()
|
| 346 |
+
})
|
| 347 |
+
except Exception as e:
|
| 348 |
+
print(f"Error parsing Google News: {e}")
|
| 349 |
+
|
| 350 |
+
return articles
|
| 351 |
+
|
| 352 |
+
def _get_fallback_data(self):
|
| 353 |
+
"""Return fallback data if real-time news couldn't be fetched"""
|
| 354 |
+
# Current timestamp for realistic data
|
| 355 |
+
current_time = datetime.now().isoformat()
|
| 356 |
+
|
| 357 |
+
return {
|
| 358 |
+
"articles": [
|
| 359 |
+
{
|
| 360 |
+
'title': "Institutional Interest in Bitcoin Continues to Grow",
|
| 361 |
+
'description': "Major financial institutions are increasingly investing in Bitcoin as a hedge against inflation and economic uncertainty. Recent regulatory clarity has provided a more secure environment for institutional adoption.",
|
| 362 |
+
'source': "Financial Trends",
|
| 363 |
+
'url': "https://example.com/bitcoin-institutional-interest",
|
| 364 |
+
'published_at': current_time,
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
'title': "Bitcoin Mining Difficulty Reaches All-Time High",
|
| 368 |
+
'description': "Bitcoin mining difficulty has adjusted upward by 5.8% this week, reaching a new all-time high. This increased difficulty reflects growing hash power on the network and continues to ensure the security of the blockchain.",
|
| 369 |
+
'source': "Crypto Analytics",
|
| 370 |
+
'url': "https://example.com/bitcoin-mining-difficulty",
|
| 371 |
+
'published_at': current_time,
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
'title': "El Salvador's Bitcoin Treasury Surpasses $100M in Profit",
|
| 375 |
+
'description': "The government of El Salvador, which adopted Bitcoin as legal tender in 2021, has reported that its Bitcoin holdings have surpassed $100 million in unrealized profit as the cryptocurrency continues its upward trend.",
|
| 376 |
+
'source': "Global Crypto News",
|
| 377 |
+
'url': "https://example.com/el-salvador-bitcoin-profit",
|
| 378 |
+
'published_at': current_time,
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
'title': "Analysis: Bitcoin Network Health Metrics at All-Time High",
|
| 382 |
+
'description': "Key Bitcoin network health metrics including hash rate, active addresses, and transaction value are all showing positive growth, suggesting robust long-term fundamentals despite short-term price volatility.",
|
| 383 |
+
'source': "Crypto Research Firm",
|
| 384 |
+
'url': "https://example.com/bitcoin-network-health",
|
| 385 |
+
'published_at': current_time,
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
'title': "Regulatory Developments Could Impact Bitcoin's Institutional Adoption",
|
| 389 |
+
'description': "Upcoming regulatory decisions in major markets could significantly impact Bitcoin's institutional adoption trajectory, with experts suggesting clarity could unleash a new wave of investment from traditional finance.",
|
| 390 |
+
'source': "Regulatory Watch",
|
| 391 |
+
'url': "https://example.com/bitcoin-regulatory-impact",
|
| 392 |
+
'published_at': current_time,
|
| 393 |
+
}
|
| 394 |
+
],
|
| 395 |
+
"count": 5,
|
| 396 |
+
"period": "Last few days (fallback data)",
|
| 397 |
+
"timestamp": current_time
|
| 398 |
+
}
|
src/crypto_analysis/tools/news_tools.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import os
|
| 2 |
+
# from typing import Dict, Any, List, Optional
|
| 3 |
+
# from datetime import datetime, timedelta
|
| 4 |
+
# import requests
|
| 5 |
+
# from crewai_tools.tools import BaseTool
|
| 6 |
+
# from crewai_tools import WebsiteSearchTool, TavilySearchTool
|
| 7 |
+
|
| 8 |
+
# class BitcoinNewsTool(BaseTool):
|
| 9 |
+
# name: str = "Bitcoin News Tool"
|
| 10 |
+
# description: str = "Fetches latest news articles about Bitcoin and cryptocurrency market"
|
| 11 |
+
|
| 12 |
+
# def __init__(self):
|
| 13 |
+
# super().__init__()
|
| 14 |
+
# self.tavily_api_key = os.getenv("TAVILY_API_KEY")
|
| 15 |
+
# self.tavily_search = TavilySearchTool()
|
| 16 |
+
# self.web_search_tool = WebsiteSearchTool()
|
| 17 |
+
|
| 18 |
+
# def _run(self, days_back: int = 3, limit: int = 10) -> Dict[str, Any]:
|
| 19 |
+
# """
|
| 20 |
+
# Fetch news about Bitcoin from various sources
|
| 21 |
+
|
| 22 |
+
# Args:
|
| 23 |
+
# days_back: Number of days to look back for news
|
| 24 |
+
# limit: Maximum number of articles to return
|
| 25 |
+
|
| 26 |
+
# Returns:
|
| 27 |
+
# Dictionary with news articles and metadata
|
| 28 |
+
# """
|
| 29 |
+
# results = []
|
| 30 |
+
|
| 31 |
+
# # Use Tavily Search API if available
|
| 32 |
+
# if self.tavily_api_key:
|
| 33 |
+
# try:
|
| 34 |
+
# # Create search query based on time frame
|
| 35 |
+
# search_query = f"latest bitcoin cryptocurrency news analysis last {days_back} days"
|
| 36 |
+
|
| 37 |
+
# # Get news from Tavily
|
| 38 |
+
# tavily_results = self.tavily_search._run(
|
| 39 |
+
# query=search_query,
|
| 40 |
+
# max_results=limit
|
| 41 |
+
# )
|
| 42 |
+
|
| 43 |
+
# if tavily_results:
|
| 44 |
+
# # Parse Tavily results
|
| 45 |
+
# if isinstance(tavily_results, str):
|
| 46 |
+
# # Process text response
|
| 47 |
+
# items = tavily_results.split('\n\n')
|
| 48 |
+
# for item in items:
|
| 49 |
+
# if len(results) >= limit:
|
| 50 |
+
# break
|
| 51 |
+
|
| 52 |
+
# # Try to extract title and content
|
| 53 |
+
# lines = item.split('\n')
|
| 54 |
+
# title = lines[0] if lines else "No title available"
|
| 55 |
+
# content = ' '.join(lines[1:]) if len(lines) > 1 else title
|
| 56 |
+
# url = None
|
| 57 |
+
# source = "Tavily Search"
|
| 58 |
+
|
| 59 |
+
# # Look for URL
|
| 60 |
+
# for line in lines:
|
| 61 |
+
# if line.startswith("http"):
|
| 62 |
+
# url = line.strip()
|
| 63 |
+
# break
|
| 64 |
+
|
| 65 |
+
# results.append({
|
| 66 |
+
# 'title': title,
|
| 67 |
+
# 'description': content,
|
| 68 |
+
# 'source': source,
|
| 69 |
+
# 'url': url,
|
| 70 |
+
# 'published_at': datetime.now().isoformat(),
|
| 71 |
+
# 'content_preview': content
|
| 72 |
+
# })
|
| 73 |
+
# elif isinstance(tavily_results, list):
|
| 74 |
+
# # Process structured results if API returns JSON
|
| 75 |
+
# for item in tavily_results:
|
| 76 |
+
# if len(results) >= limit:
|
| 77 |
+
# break
|
| 78 |
+
|
| 79 |
+
# if isinstance(item, dict):
|
| 80 |
+
# results.append({
|
| 81 |
+
# 'title': item.get('title', 'No title available'),
|
| 82 |
+
# 'description': item.get('content', item.get('snippet', '')),
|
| 83 |
+
# 'source': item.get('source', 'Tavily Search'),
|
| 84 |
+
# 'url': item.get('url'),
|
| 85 |
+
# 'published_at': datetime.now().isoformat(),
|
| 86 |
+
# 'content_preview': item.get('content', item.get('snippet', ''))
|
| 87 |
+
# })
|
| 88 |
+
# except Exception as e:
|
| 89 |
+
# print(f"Error fetching news from Tavily: {e}")
|
| 90 |
+
|
| 91 |
+
# # If we have insufficient results or Tavily is not available, use web search
|
| 92 |
+
# if len(results) < limit:
|
| 93 |
+
# try:
|
| 94 |
+
# additional_needed = limit - len(results)
|
| 95 |
+
# web_search_results = self.web_search_tool._run(
|
| 96 |
+
# query=f"latest bitcoin news crypto market analysis last {days_back} days"
|
| 97 |
+
# )
|
| 98 |
+
|
| 99 |
+
# # Parse and add web search results if available
|
| 100 |
+
# if isinstance(web_search_results, str) and len(web_search_results) > 0:
|
| 101 |
+
# # Simple parsing - assume each line might contain a news item
|
| 102 |
+
# lines = web_search_results.split('\n')
|
| 103 |
+
# for line in lines:
|
| 104 |
+
# if len(line.strip()) > 0 and len(results) < limit:
|
| 105 |
+
# # Try to extract a title and URL
|
| 106 |
+
# parts = line.split(' - ')
|
| 107 |
+
# title = parts[0] if len(parts) > 0 else line
|
| 108 |
+
# source = parts[1] if len(parts) > 1 else "Web Search"
|
| 109 |
+
|
| 110 |
+
# results.append({
|
| 111 |
+
# 'title': title,
|
| 112 |
+
# 'description': line,
|
| 113 |
+
# 'source': source,
|
| 114 |
+
# 'url': None, # URL not available from simple parsing
|
| 115 |
+
# 'published_at': None, # Date not available
|
| 116 |
+
# 'content_preview': line
|
| 117 |
+
# })
|
| 118 |
+
|
| 119 |
+
# if len(results) >= limit:
|
| 120 |
+
# break
|
| 121 |
+
# except Exception as e:
|
| 122 |
+
# print(f"Error fetching news from web search: {e}")
|
| 123 |
+
|
| 124 |
+
# return {
|
| 125 |
+
# "articles": results,
|
| 126 |
+
# "count": len(results),
|
| 127 |
+
# "period": f"Last {days_back} days",
|
| 128 |
+
# "timestamp": datetime.now().isoformat()
|
| 129 |
+
# }
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# class BitcoinSentimentTool(BaseTool):
|
| 133 |
+
# name: str = "Bitcoin Sentiment Analysis Tool"
|
| 134 |
+
# description: str = "Analyzes sentiment from recent Bitcoin news and social media data"
|
| 135 |
+
|
| 136 |
+
# def __init__(self):
|
| 137 |
+
# super().__init__()
|
| 138 |
+
# self.news_tool = BitcoinNewsTool()
|
| 139 |
+
|
| 140 |
+
# def _run(self, days_back: int = 3) -> Dict[str, Any]:
|
| 141 |
+
# """
|
| 142 |
+
# Analyze sentiment from Bitcoin news
|
| 143 |
+
|
| 144 |
+
# Args:
|
| 145 |
+
# days_back: Number of days to look back for analysis
|
| 146 |
+
|
| 147 |
+
# Returns:
|
| 148 |
+
# Dictionary with sentiment analysis results
|
| 149 |
+
# """
|
| 150 |
+
# # Get news data
|
| 151 |
+
# news_data = self.news_tool._run(days_back=days_back, limit=15)
|
| 152 |
+
|
| 153 |
+
# if "articles" not in news_data or len(news_data["articles"]) == 0:
|
| 154 |
+
# return {
|
| 155 |
+
# "error": "No news articles found for analysis",
|
| 156 |
+
# "sentiment_score": 0,
|
| 157 |
+
# "sentiment": "neutral",
|
| 158 |
+
# "confidence": 0
|
| 159 |
+
# }
|
| 160 |
+
|
| 161 |
+
# # Extract titles and descriptions for sentiment analysis
|
| 162 |
+
# texts = []
|
| 163 |
+
# for article in news_data["articles"]:
|
| 164 |
+
# title = article.get("title", "")
|
| 165 |
+
# description = article.get("description", "")
|
| 166 |
+
|
| 167 |
+
# if title and len(title) > 0:
|
| 168 |
+
# texts.append(title)
|
| 169 |
+
# if description and len(description) > 0:
|
| 170 |
+
# texts.append(description)
|
| 171 |
+
|
| 172 |
+
# # In a real implementation, you would use a proper sentiment analysis API here
|
| 173 |
+
# # For this example, we'll use a simple keyword-based approach
|
| 174 |
+
# positive_keywords = [
|
| 175 |
+
# "surge", "rally", "bullish", "growth", "positive", "gain", "gains", "uptrend",
|
| 176 |
+
# "soar", "adoption", "breakthrough", "opportunity", "profit", "boost", "climb",
|
| 177 |
+
# "optimistic", "confidence", "successful", "legalization", "mainstream",
|
| 178 |
+
# "institutional", "hodl", "to the moon", "recovery"
|
| 179 |
+
# ]
|
| 180 |
+
|
| 181 |
+
# negative_keywords = [
|
| 182 |
+
# "crash", "fall", "bearish", "drop", "negative", "loss", "losses", "downtrend",
|
| 183 |
+
# "plummet", "ban", "regulation", "risk", "caution", "warning", "decline",
|
| 184 |
+
# "pessimistic", "fear", "sell-off", "correction", "bubble", "volatility",
|
| 185 |
+
# "scandal", "hack", "fraud", "manipulation", "uncertainty"
|
| 186 |
+
# ]
|
| 187 |
+
|
| 188 |
+
# # Count occurrences of positive and negative keywords
|
| 189 |
+
# positive_count = 0
|
| 190 |
+
# negative_count = 0
|
| 191 |
+
|
| 192 |
+
# for text in texts:
|
| 193 |
+
# if text:
|
| 194 |
+
# text_lower = text.lower()
|
| 195 |
+
# for keyword in positive_keywords:
|
| 196 |
+
# if keyword.lower() in text_lower:
|
| 197 |
+
# positive_count += 1
|
| 198 |
+
|
| 199 |
+
# for keyword in negative_keywords:
|
| 200 |
+
# if keyword.lower() in text_lower:
|
| 201 |
+
# negative_count += 1
|
| 202 |
+
|
| 203 |
+
# # Calculate sentiment score (-1 to 1)
|
| 204 |
+
# total_count = positive_count + negative_count
|
| 205 |
+
# if total_count == 0:
|
| 206 |
+
# sentiment_score = 0
|
| 207 |
+
# else:
|
| 208 |
+
# sentiment_score = (positive_count - negative_count) / total_count
|
| 209 |
+
|
| 210 |
+
# # Determine sentiment category
|
| 211 |
+
# if sentiment_score > 0.2:
|
| 212 |
+
# sentiment = "positive"
|
| 213 |
+
# elif sentiment_score < -0.2:
|
| 214 |
+
# sentiment = "negative"
|
| 215 |
+
# else:
|
| 216 |
+
# sentiment = "neutral"
|
| 217 |
+
|
| 218 |
+
# # Calculate confidence (higher count = higher confidence)
|
| 219 |
+
# confidence = min(total_count / max(1, len(texts)), 1.0) * 100
|
| 220 |
+
|
| 221 |
+
# return {
|
| 222 |
+
# "sentiment_score": sentiment_score,
|
| 223 |
+
# "sentiment": sentiment,
|
| 224 |
+
# "confidence": confidence,
|
| 225 |
+
# "positive_mentions": positive_count,
|
| 226 |
+
# "negative_mentions": negative_count,
|
| 227 |
+
# "total_articles": len(news_data["articles"]),
|
| 228 |
+
# "period": f"Last {days_back} days",
|
| 229 |
+
# "timestamp": datetime.now().isoformat()
|
| 230 |
+
# }
|
src/crypto_analysis/tools/order_tools.py
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, Any, Optional, Union
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from crewai.tools import BaseTool
|
| 7 |
+
from pydantic import Field, BaseModel
|
| 8 |
+
|
| 9 |
+
# Create a specific schema for the tool input
|
| 10 |
+
class AlpacaOrderInput(BaseModel):
|
| 11 |
+
action: str = Field(description="'buy', 'sell', or 'check' to check account")
|
| 12 |
+
symbol: str = Field(default="BTC/USD", description="Crypto trading pair (e.g., BTC/USD)")
|
| 13 |
+
quantity: Optional[float] = Field(default=None, description="Specific quantity to trade")
|
| 14 |
+
allocation_percentage: Optional[int] = Field(default=None, description="Percentage of portfolio to allocate")
|
| 15 |
+
confidence: Optional[int] = Field(default=None, description="Confidence level (0-100)")
|
| 16 |
+
|
| 17 |
+
class AlpacaCryptoOrderTool(BaseTool):
|
| 18 |
+
name: str = "Alpaca Crypto Order Execution Tool"
|
| 19 |
+
description: str = "Execute cryptocurrency buy/sell orders on Alpaca based on trading signals"
|
| 20 |
+
|
| 21 |
+
# Define fields
|
| 22 |
+
api_key: Optional[str] = Field(default=None, description="Alpaca API key")
|
| 23 |
+
api_secret: Optional[str] = Field(default=None, description="Alpaca API secret")
|
| 24 |
+
is_paper: bool = Field(default=True, description="Whether to use paper trading API")
|
| 25 |
+
base_url: str = Field(default="https://paper-api.alpaca.markets", description="API base URL for trading")
|
| 26 |
+
data_url: str = Field(default="https://data.alpaca.markets", description="API base URL for market data")
|
| 27 |
+
|
| 28 |
+
# Define the schema for the arguments
|
| 29 |
+
args_schema: type[AlpacaOrderInput] = AlpacaOrderInput
|
| 30 |
+
|
| 31 |
+
def __init__(self, **kwargs):
|
| 32 |
+
# Initialize the base class first
|
| 33 |
+
super().__init__(**kwargs)
|
| 34 |
+
|
| 35 |
+
# Set the API keys from environment variables if not provided directly
|
| 36 |
+
if not self.api_key:
|
| 37 |
+
self.api_key = os.getenv("ALPACA_API_KEY")
|
| 38 |
+
if not self.api_secret:
|
| 39 |
+
self.api_secret = os.getenv("ALPACA_API_SECRET")
|
| 40 |
+
|
| 41 |
+
# Set the API base URL based on whether we're using paper trading (only for trading API)
|
| 42 |
+
if 'base_url' not in kwargs: # Only set if not provided directly
|
| 43 |
+
self.base_url = "https://paper-api.alpaca.markets" if self.is_paper else "https://api.alpaca.markets"
|
| 44 |
+
|
| 45 |
+
print(f"Initialized Alpaca Order Tool with API key: {'Present' if self.api_key else 'Missing'}")
|
| 46 |
+
print(f"Using API endpoints: Trading={self.base_url}, Data={self.data_url}")
|
| 47 |
+
|
| 48 |
+
def _run(self, **kwargs) -> Dict[str, Any]:
|
| 49 |
+
"""
|
| 50 |
+
Execute a crypto order on Alpaca
|
| 51 |
+
|
| 52 |
+
Args can be provided either as individual parameters or as kwargs:
|
| 53 |
+
action: 'buy', 'sell', or 'check' (to check account)
|
| 54 |
+
symbol: Crypto symbol to trade (e.g., "BTC/USD")
|
| 55 |
+
quantity: Specific quantity to trade (e.g., 0.001 BTC)
|
| 56 |
+
allocation_percentage: Alternative to quantity - percentage of portfolio to allocate
|
| 57 |
+
confidence: Optional confidence level (0-100) to include in order metadata
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
Dictionary with order results
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
# Extract parameters from kwargs
|
| 64 |
+
action = kwargs.get('action')
|
| 65 |
+
symbol = kwargs.get('symbol', 'BTC/USD')
|
| 66 |
+
quantity = kwargs.get('quantity')
|
| 67 |
+
allocation_percentage = kwargs.get('allocation_percentage')
|
| 68 |
+
confidence = kwargs.get('confidence')
|
| 69 |
+
|
| 70 |
+
# Log all inputs for debugging
|
| 71 |
+
print(f"Raw input kwargs: {kwargs}")
|
| 72 |
+
print(f"Extracted params: action={action}, symbol={symbol}, quantity={quantity}, allocation_percentage={allocation_percentage}, confidence={confidence}")
|
| 73 |
+
|
| 74 |
+
# Validate action is provided
|
| 75 |
+
if not action:
|
| 76 |
+
print("Missing required parameter: action")
|
| 77 |
+
return {
|
| 78 |
+
"error": "Missing required parameter: action. Must be 'buy', 'sell', or 'check'.",
|
| 79 |
+
"success": False
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# Ensure proper symbol format (convert BTCUSD to BTC/USD if needed)
|
| 83 |
+
if symbol == "BTCUSD":
|
| 84 |
+
symbol = "BTC/USD"
|
| 85 |
+
print(f"Corrected symbol format from BTCUSD to {symbol}")
|
| 86 |
+
elif symbol == "ETHUSD":
|
| 87 |
+
symbol = "ETH/USD"
|
| 88 |
+
print(f"Corrected symbol format from ETHUSD to {symbol}")
|
| 89 |
+
|
| 90 |
+
print(f"Order Tool called with action={action}, symbol={symbol}, quantity={quantity}, allocation_percentage={allocation_percentage}")
|
| 91 |
+
|
| 92 |
+
# Validate action
|
| 93 |
+
if action.lower() not in ["buy", "sell", "check"]:
|
| 94 |
+
print(f"Invalid action: {action}")
|
| 95 |
+
return {
|
| 96 |
+
"error": f"Invalid action: {action}. Use 'buy', 'sell', or 'check'.",
|
| 97 |
+
"success": False
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
# Check account status if requested
|
| 101 |
+
if action.lower() == "check":
|
| 102 |
+
print("Checking account status")
|
| 103 |
+
account_info = self._check_account()
|
| 104 |
+
if account_info.get("success", False):
|
| 105 |
+
print(f"Account check successful: Cash={account_info.get('cash')}, Equity={account_info.get('equity')}")
|
| 106 |
+
return account_info
|
| 107 |
+
|
| 108 |
+
# Ensure we have valid credentials
|
| 109 |
+
if not self.api_key or not self.api_secret:
|
| 110 |
+
print("Missing API credentials")
|
| 111 |
+
return {
|
| 112 |
+
"error": "Missing API credentials. Please set ALPACA_API_KEY and ALPACA_API_SECRET environment variables.",
|
| 113 |
+
"success": False
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# For buy/sell, validate we have either quantity or allocation_percentage
|
| 117 |
+
if action.lower() in ["buy", "sell"] and quantity is None and allocation_percentage is None:
|
| 118 |
+
print("Missing both quantity and allocation_percentage")
|
| 119 |
+
return {
|
| 120 |
+
"error": "For buy/sell orders, you must provide either quantity OR allocation_percentage",
|
| 121 |
+
"success": False
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Convert allocation_percentage to integer if it's a string
|
| 125 |
+
if allocation_percentage is not None and isinstance(allocation_percentage, str):
|
| 126 |
+
try:
|
| 127 |
+
allocation_percentage = int(allocation_percentage.strip('%'))
|
| 128 |
+
print(f"Converted allocation_percentage from string to int: {allocation_percentage}")
|
| 129 |
+
except ValueError:
|
| 130 |
+
return {
|
| 131 |
+
"error": f"Invalid allocation_percentage format: {allocation_percentage}. Must be an integer or percentage string.",
|
| 132 |
+
"success": False
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
# Convert quantity to float if it's a string
|
| 136 |
+
if quantity is not None and isinstance(quantity, str):
|
| 137 |
+
try:
|
| 138 |
+
quantity = float(quantity)
|
| 139 |
+
print(f"Converted quantity from string to float: {quantity}")
|
| 140 |
+
except ValueError:
|
| 141 |
+
return {
|
| 142 |
+
"error": f"Invalid quantity format: {quantity}. Must be a number.",
|
| 143 |
+
"success": False
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
# Determine the quantity based on inputs
|
| 147 |
+
final_quantity = None
|
| 148 |
+
|
| 149 |
+
# For 'sell' action with allocation percentage, we need to check current positions
|
| 150 |
+
if action.lower() == "sell" and allocation_percentage is not None:
|
| 151 |
+
print(f"Selling with allocation percentage: {allocation_percentage}%")
|
| 152 |
+
# Get current positions to determine how much to sell
|
| 153 |
+
positions = self._get_positions(symbol)
|
| 154 |
+
if not positions:
|
| 155 |
+
return {
|
| 156 |
+
"error": f"No position found for {symbol} to sell",
|
| 157 |
+
"success": False,
|
| 158 |
+
"positions_checked": True
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
# Calculate quantity to sell based on allocation percentage
|
| 162 |
+
position_qty = float(positions.get("qty", 0))
|
| 163 |
+
sell_qty = position_qty * (allocation_percentage / 100.0)
|
| 164 |
+
|
| 165 |
+
# Ensure minimum order size
|
| 166 |
+
min_order_size = 0.0001 if "BTC" in symbol else 0.001
|
| 167 |
+
if sell_qty < min_order_size:
|
| 168 |
+
sell_qty = min(min_order_size, position_qty)
|
| 169 |
+
|
| 170 |
+
final_quantity = round(sell_qty, 8)
|
| 171 |
+
print(f"Calculated sell quantity: {final_quantity} from position quantity: {position_qty}")
|
| 172 |
+
|
| 173 |
+
elif quantity is not None:
|
| 174 |
+
# Direct quantity specification
|
| 175 |
+
final_quantity = quantity
|
| 176 |
+
print(f"Using directly specified quantity: {final_quantity}")
|
| 177 |
+
|
| 178 |
+
elif allocation_percentage is not None:
|
| 179 |
+
# Calculate quantity based on allocation percentage
|
| 180 |
+
print(f"Calculating buy quantity based on allocation percentage: {allocation_percentage}%")
|
| 181 |
+
account_info = self._check_account()
|
| 182 |
+
if "error" in account_info:
|
| 183 |
+
return account_info
|
| 184 |
+
|
| 185 |
+
# Get account cash and calculate notional amount to trade
|
| 186 |
+
cash = float(account_info.get("cash", 0))
|
| 187 |
+
equity = float(account_info.get("equity", cash)) # Use equity if available, otherwise cash
|
| 188 |
+
|
| 189 |
+
# For allocation, we should use total equity for a more accurate percentage
|
| 190 |
+
allocation_amount = equity * (allocation_percentage / 100.0)
|
| 191 |
+
|
| 192 |
+
print(f"Account equity: ${equity}, Cash: ${cash}, Allocation amount: ${allocation_amount}")
|
| 193 |
+
|
| 194 |
+
# Get current price to calculate quantity
|
| 195 |
+
current_price = self._get_current_price(symbol)
|
| 196 |
+
if current_price is None:
|
| 197 |
+
# We've already tried all price retrieval methods and failed
|
| 198 |
+
# Check if this is a BUY order - we can use a default quantity
|
| 199 |
+
if action.lower() == "buy":
|
| 200 |
+
# For buy orders, we can use a default minimum quantity
|
| 201 |
+
min_order_size = 0.0001 if "BTC" in symbol else 0.001
|
| 202 |
+
print(f"WARNING: Price retrieval failed. Using minimum order size: {min_order_size}")
|
| 203 |
+
|
| 204 |
+
# Verify we have enough cash for minimum order
|
| 205 |
+
estimated_btc_price = 60000.0 # Fallback estimate for BTC price
|
| 206 |
+
estimated_cost = min_order_size * estimated_btc_price
|
| 207 |
+
|
| 208 |
+
if cash >= estimated_cost * 1.05: # Add 5% buffer for price fluctuations
|
| 209 |
+
print(f"Proceeding with minimum order using estimated price ~${estimated_btc_price}")
|
| 210 |
+
final_quantity = min_order_size
|
| 211 |
+
else:
|
| 212 |
+
return {
|
| 213 |
+
"error": f"Failed to get current price for {symbol} and insufficient cash for minimum order",
|
| 214 |
+
"success": False,
|
| 215 |
+
"price_retrieval_failed": True
|
| 216 |
+
}
|
| 217 |
+
else:
|
| 218 |
+
# For sell orders, we need the current price
|
| 219 |
+
return {
|
| 220 |
+
"error": f"Failed to get current price for {symbol}. The account status is verified with sufficient funds available for execution when the issue is resolved.",
|
| 221 |
+
"success": False,
|
| 222 |
+
"price_retrieval_failed": True
|
| 223 |
+
}
|
| 224 |
+
else:
|
| 225 |
+
print(f"Current price for {symbol}: ${current_price}")
|
| 226 |
+
|
| 227 |
+
# Calculate exact quantity of BTC to buy based on allocation amount and price
|
| 228 |
+
final_quantity = allocation_amount / current_price
|
| 229 |
+
|
| 230 |
+
# Format to appropriate number of decimal places based on symbol
|
| 231 |
+
if "BTC" in symbol:
|
| 232 |
+
final_quantity = round(final_quantity, 8) # 8 decimal places for BTC
|
| 233 |
+
else:
|
| 234 |
+
final_quantity = round(final_quantity, 4) # 4 decimal places for most other cryptos
|
| 235 |
+
|
| 236 |
+
print(f"Calculated quantity to {action}: {final_quantity} BTC (worth ${allocation_amount:.2f})")
|
| 237 |
+
|
| 238 |
+
# Set minimum order size
|
| 239 |
+
min_order_size = 0.0001 if "BTC" in symbol else 0.001
|
| 240 |
+
if final_quantity < min_order_size:
|
| 241 |
+
print(f"Calculated quantity {final_quantity} is below minimum order size {min_order_size}")
|
| 242 |
+
if allocation_amount > 5: # Only place order if allocation is meaningful
|
| 243 |
+
final_quantity = min_order_size
|
| 244 |
+
print(f"Setting to minimum order size: {min_order_size} BTC")
|
| 245 |
+
else:
|
| 246 |
+
return {
|
| 247 |
+
"error": f"Allocation amount ${allocation_amount:.2f} too small for minimum order size of {min_order_size} BTC (${min_order_size * current_price:.2f})",
|
| 248 |
+
"success": False,
|
| 249 |
+
"allocation_too_small": True
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
if final_quantity is None or final_quantity <= 0:
|
| 253 |
+
print("Invalid quantity determined")
|
| 254 |
+
return {
|
| 255 |
+
"error": "Invalid quantity. Please provide either a valid quantity or allocation percentage.",
|
| 256 |
+
"success": False
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
# Execute the order
|
| 260 |
+
print(f"Placing {action} order for {final_quantity} of {symbol}")
|
| 261 |
+
order_result = self._place_order(
|
| 262 |
+
symbol=symbol,
|
| 263 |
+
side=action.lower(),
|
| 264 |
+
quantity=final_quantity,
|
| 265 |
+
confidence=confidence
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
if order_result.get("success", False):
|
| 269 |
+
print(f"Order placed successfully: {order_result.get('order_id')}")
|
| 270 |
+
else:
|
| 271 |
+
print(f"Order failed: {order_result.get('error')}")
|
| 272 |
+
|
| 273 |
+
return order_result
|
| 274 |
+
|
| 275 |
+
except Exception as e:
|
| 276 |
+
print(f"Error executing order: {str(e)}")
|
| 277 |
+
return {
|
| 278 |
+
"error": f"Error executing order: {str(e)}",
|
| 279 |
+
"success": False
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
def _check_account(self) -> Dict[str, Any]:
|
| 283 |
+
"""Get account information from Alpaca"""
|
| 284 |
+
try:
|
| 285 |
+
url = f"{self.base_url}/v2/account"
|
| 286 |
+
headers = {
|
| 287 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 288 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
response = requests.get(url, headers=headers)
|
| 292 |
+
|
| 293 |
+
if response.status_code != 200:
|
| 294 |
+
return {
|
| 295 |
+
"error": f"Failed to get account info: {response.text}",
|
| 296 |
+
"success": False
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
account_data = response.json()
|
| 300 |
+
|
| 301 |
+
return {
|
| 302 |
+
"account_id": account_data.get("id"),
|
| 303 |
+
"cash": account_data.get("cash"),
|
| 304 |
+
"equity": account_data.get("equity"),
|
| 305 |
+
"buying_power": account_data.get("buying_power"),
|
| 306 |
+
"success": True
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
return {
|
| 311 |
+
"error": f"Error checking account: {str(e)}",
|
| 312 |
+
"success": False
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
def _get_current_price(self, symbol: str) -> Optional[float]:
|
| 316 |
+
"""Get the current price of a crypto asset"""
|
| 317 |
+
try:
|
| 318 |
+
# Ensure proper symbol format (convert BTCUSD to BTC/USD if needed)
|
| 319 |
+
if symbol == "BTCUSD":
|
| 320 |
+
symbol = "BTC/USD"
|
| 321 |
+
print(f"Corrected symbol format from BTCUSD to {symbol} in _get_current_price")
|
| 322 |
+
elif symbol == "ETHUSD":
|
| 323 |
+
symbol = "ETH/USD"
|
| 324 |
+
print(f"Corrected symbol format from ETHUSD to {symbol} in _get_current_price")
|
| 325 |
+
|
| 326 |
+
# First attempt: Use latest bars endpoint
|
| 327 |
+
print(f"Attempting to get price for {symbol} using latest bars endpoint")
|
| 328 |
+
price = self._try_get_price_from_bars(symbol)
|
| 329 |
+
if price is not None:
|
| 330 |
+
return price
|
| 331 |
+
|
| 332 |
+
# Second attempt: Try the snapshots endpoint
|
| 333 |
+
print(f"Attempting to get price for {symbol} using snapshots endpoint")
|
| 334 |
+
price = self._try_get_price_from_snapshots(symbol)
|
| 335 |
+
if price is not None:
|
| 336 |
+
return price
|
| 337 |
+
|
| 338 |
+
# Third attempt: Try orderbook endpoint
|
| 339 |
+
print(f"Attempting to get price for {symbol} using orderbook endpoint")
|
| 340 |
+
price = self._try_get_price_from_orderbook(symbol)
|
| 341 |
+
if price is not None:
|
| 342 |
+
return price
|
| 343 |
+
|
| 344 |
+
# If all attempts fail, use a fallback hardcoded price
|
| 345 |
+
print(f"WARNING: Could not get price for {symbol} from any API endpoint. Using fallback price.")
|
| 346 |
+
fallback_prices = {
|
| 347 |
+
"BTC/USD": 85000.00, # Approximate BTC price as fallback
|
| 348 |
+
}
|
| 349 |
+
if symbol in fallback_prices:
|
| 350 |
+
print(f"Using fallback price for {symbol}: ${fallback_prices[symbol]}")
|
| 351 |
+
return fallback_prices[symbol]
|
| 352 |
+
|
| 353 |
+
return None
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
print(f"Error getting price: {str(e)}")
|
| 357 |
+
return None
|
| 358 |
+
|
| 359 |
+
def _try_get_price_from_bars(self, symbol: str) -> Optional[float]:
|
| 360 |
+
"""Try to get price from the bars endpoint"""
|
| 361 |
+
try:
|
| 362 |
+
url = f"{self.data_url}/v1beta3/crypto/us/latest/bars"
|
| 363 |
+
headers = {
|
| 364 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 365 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 366 |
+
}
|
| 367 |
+
params = {"symbols": symbol}
|
| 368 |
+
|
| 369 |
+
print(f"Getting current price from bars endpoint: {url} for symbol {symbol}")
|
| 370 |
+
response = requests.get(url, headers=headers, params=params)
|
| 371 |
+
|
| 372 |
+
if response.status_code != 200:
|
| 373 |
+
print(f"Error getting price from bars: {response.status_code} - {response.text}")
|
| 374 |
+
return None
|
| 375 |
+
|
| 376 |
+
data = response.json()
|
| 377 |
+
print(f"Bars response: {json.dumps(data)[:300]}...")
|
| 378 |
+
|
| 379 |
+
if "bars" not in data or symbol not in data["bars"] or not data["bars"][symbol]:
|
| 380 |
+
print(f"No bar data found for {symbol}")
|
| 381 |
+
return None
|
| 382 |
+
|
| 383 |
+
# Return the close price of the latest bar
|
| 384 |
+
return float(data["bars"][symbol][0]["c"])
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
print(f"Error in _try_get_price_from_bars: {str(e)}")
|
| 388 |
+
return None
|
| 389 |
+
|
| 390 |
+
def _try_get_price_from_snapshots(self, symbol: str) -> Optional[float]:
|
| 391 |
+
"""Try to get price from the snapshots endpoint"""
|
| 392 |
+
try:
|
| 393 |
+
url = f"{self.data_url}/v1beta3/crypto/us/snapshots"
|
| 394 |
+
headers = {
|
| 395 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 396 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 397 |
+
}
|
| 398 |
+
params = {"symbols": symbol}
|
| 399 |
+
|
| 400 |
+
print(f"Getting current price from snapshots endpoint: {url} for symbol {symbol}")
|
| 401 |
+
response = requests.get(url, headers=headers, params=params)
|
| 402 |
+
|
| 403 |
+
if response.status_code != 200:
|
| 404 |
+
print(f"Error getting price from snapshots: {response.status_code} - {response.text}")
|
| 405 |
+
return None
|
| 406 |
+
|
| 407 |
+
data = response.json()
|
| 408 |
+
print(f"Snapshots response: {json.dumps(data)[:300]}...")
|
| 409 |
+
|
| 410 |
+
if "snapshots" not in data or symbol not in data["snapshots"]:
|
| 411 |
+
print(f"No snapshot data found for {symbol}")
|
| 412 |
+
return None
|
| 413 |
+
|
| 414 |
+
# Try to get price from latest bar in the snapshot
|
| 415 |
+
snapshot = data["snapshots"][symbol]
|
| 416 |
+
if "bar" in snapshot and snapshot["bar"]:
|
| 417 |
+
return float(snapshot["bar"]["c"])
|
| 418 |
+
|
| 419 |
+
# Try to get price from latest quote in the snapshot
|
| 420 |
+
if "quote" in snapshot and snapshot["quote"]:
|
| 421 |
+
ask = snapshot["quote"].get("ap")
|
| 422 |
+
bid = snapshot["quote"].get("bp")
|
| 423 |
+
if ask and bid:
|
| 424 |
+
return (float(ask) + float(bid)) / 2 # Average of bid and ask
|
| 425 |
+
elif ask:
|
| 426 |
+
return float(ask)
|
| 427 |
+
elif bid:
|
| 428 |
+
return float(bid)
|
| 429 |
+
|
| 430 |
+
return None
|
| 431 |
+
|
| 432 |
+
except Exception as e:
|
| 433 |
+
print(f"Error in _try_get_price_from_snapshots: {str(e)}")
|
| 434 |
+
return None
|
| 435 |
+
|
| 436 |
+
def _try_get_price_from_orderbook(self, symbol: str) -> Optional[float]:
|
| 437 |
+
"""Try to get price from the orderbook endpoint"""
|
| 438 |
+
try:
|
| 439 |
+
url = f"{self.data_url}/v1beta3/crypto/us/latest/orderbooks"
|
| 440 |
+
headers = {
|
| 441 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 442 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 443 |
+
}
|
| 444 |
+
params = {"symbols": symbol}
|
| 445 |
+
|
| 446 |
+
print(f"Getting current price from orderbook endpoint: {url} for symbol {symbol}")
|
| 447 |
+
response = requests.get(url, headers=headers, params=params)
|
| 448 |
+
|
| 449 |
+
if response.status_code != 200:
|
| 450 |
+
print(f"Error getting price from orderbook: {response.status_code} - {response.text}")
|
| 451 |
+
return None
|
| 452 |
+
|
| 453 |
+
data = response.json()
|
| 454 |
+
print(f"Orderbook response: {json.dumps(data)[:300]}...")
|
| 455 |
+
|
| 456 |
+
if "orderbooks" not in data or symbol not in data["orderbooks"]:
|
| 457 |
+
print(f"No orderbook data found for {symbol}")
|
| 458 |
+
return None
|
| 459 |
+
|
| 460 |
+
orderbook = data["orderbooks"][symbol]
|
| 461 |
+
|
| 462 |
+
# Try to get mid price from best bid and ask
|
| 463 |
+
if "a" in orderbook and orderbook["a"] and "b" in orderbook and orderbook["b"]:
|
| 464 |
+
best_ask = float(orderbook["a"][0]["p"]) if orderbook["a"] else None
|
| 465 |
+
best_bid = float(orderbook["b"][0]["p"]) if orderbook["b"] else None
|
| 466 |
+
|
| 467 |
+
if best_ask and best_bid:
|
| 468 |
+
return (best_ask + best_bid) / 2 # Mid price
|
| 469 |
+
elif best_ask:
|
| 470 |
+
return best_ask
|
| 471 |
+
elif best_bid:
|
| 472 |
+
return best_bid
|
| 473 |
+
|
| 474 |
+
return None
|
| 475 |
+
|
| 476 |
+
except Exception as e:
|
| 477 |
+
print(f"Error in _try_get_price_from_orderbook: {str(e)}")
|
| 478 |
+
return None
|
| 479 |
+
|
| 480 |
+
def _place_order(self, symbol: str, side: str, quantity: float, confidence: Optional[int] = None) -> Dict[str, Any]:
|
| 481 |
+
"""Place a crypto order on Alpaca"""
|
| 482 |
+
try:
|
| 483 |
+
# Ensure proper symbol format
|
| 484 |
+
if symbol == "BTCUSD":
|
| 485 |
+
symbol = "BTC/USD"
|
| 486 |
+
print(f"Corrected symbol format from BTCUSD to {symbol} in _place_order")
|
| 487 |
+
elif symbol == "ETHUSD":
|
| 488 |
+
symbol = "ETH/USD"
|
| 489 |
+
print(f"Corrected symbol format from ETHUSD to {symbol} in _place_order")
|
| 490 |
+
|
| 491 |
+
url = f"{self.base_url}/v2/orders"
|
| 492 |
+
|
| 493 |
+
headers = {
|
| 494 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 495 |
+
"Apca-Api-Secret-Key": self.api_secret,
|
| 496 |
+
"Content-Type": "application/json"
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
# Ensure quantity is properly formatted
|
| 500 |
+
# Alpaca requires qty as a string with appropriate precision
|
| 501 |
+
if "BTC" in symbol:
|
| 502 |
+
qty_str = f"{quantity:.8f}".rstrip('0').rstrip('.') if '.' in f"{quantity:.8f}" else f"{quantity:.8f}"
|
| 503 |
+
else:
|
| 504 |
+
qty_str = f"{quantity:.4f}".rstrip('0').rstrip('.') if '.' in f"{quantity:.4f}" else f"{quantity:.4f}"
|
| 505 |
+
|
| 506 |
+
print(f"Formatting quantity {quantity} as '{qty_str}' for API call")
|
| 507 |
+
|
| 508 |
+
# Prepare the order data
|
| 509 |
+
order_data = {
|
| 510 |
+
"symbol": symbol,
|
| 511 |
+
"qty": qty_str, # Use formatted string
|
| 512 |
+
"side": side,
|
| 513 |
+
"type": "market",
|
| 514 |
+
"time_in_force": "gtc"
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
print(f"Sending order data to Alpaca: {json.dumps(order_data)}")
|
| 518 |
+
|
| 519 |
+
# Add custom metadata if confidence is provided
|
| 520 |
+
if confidence is not None:
|
| 521 |
+
order_data["client_order_id"] = f"signal-{confidence}-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
| 522 |
+
|
| 523 |
+
# Send the order request
|
| 524 |
+
response = requests.post(url, headers=headers, json=order_data)
|
| 525 |
+
|
| 526 |
+
if response.status_code not in [200, 201]:
|
| 527 |
+
return {
|
| 528 |
+
"error": f"Failed to place order: {response.text}",
|
| 529 |
+
"success": False
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
# Parse the response
|
| 533 |
+
order_response = response.json()
|
| 534 |
+
|
| 535 |
+
# Get the current price for reference
|
| 536 |
+
current_price = self._get_current_price(symbol)
|
| 537 |
+
|
| 538 |
+
# Return order details
|
| 539 |
+
return {
|
| 540 |
+
"order_id": order_response.get("id"),
|
| 541 |
+
"client_order_id": order_response.get("client_order_id"),
|
| 542 |
+
"symbol": order_response.get("symbol"),
|
| 543 |
+
"side": order_response.get("side"),
|
| 544 |
+
"quantity": order_response.get("qty"),
|
| 545 |
+
"order_type": order_response.get("type"),
|
| 546 |
+
"status": order_response.get("status"),
|
| 547 |
+
"current_price": current_price,
|
| 548 |
+
"created_at": order_response.get("created_at"),
|
| 549 |
+
"confidence": confidence,
|
| 550 |
+
"success": True
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
except Exception as e:
|
| 554 |
+
return {
|
| 555 |
+
"error": f"Error placing order: {str(e)}",
|
| 556 |
+
"success": False
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
def _get_positions(self, symbol: str = None) -> Dict[str, Any]:
|
| 560 |
+
"""Get current positions from Alpaca"""
|
| 561 |
+
try:
|
| 562 |
+
# Ensure proper symbol format if provided
|
| 563 |
+
original_symbol = symbol
|
| 564 |
+
|
| 565 |
+
# Normalize the symbol for comparison - can come in various formats
|
| 566 |
+
normalized_symbol = None
|
| 567 |
+
if symbol:
|
| 568 |
+
# Remove '/' for comparison if present
|
| 569 |
+
if '/' in symbol:
|
| 570 |
+
normalized_symbol = symbol.replace('/', '')
|
| 571 |
+
else:
|
| 572 |
+
# Add '/' if not present (e.g., BTCUSD -> BTC/USD)
|
| 573 |
+
if symbol == "BTCUSD":
|
| 574 |
+
normalized_symbol = "BTC/USD"
|
| 575 |
+
symbol = "BTC/USD"
|
| 576 |
+
elif symbol == "ETHUSD":
|
| 577 |
+
normalized_symbol = "ETH/USD"
|
| 578 |
+
symbol = "ETH/USD"
|
| 579 |
+
else:
|
| 580 |
+
# Try to insert a slash at the right place for other symbols
|
| 581 |
+
if len(symbol) >= 6 and symbol[0:3].isalpha() and symbol[3:].isalpha():
|
| 582 |
+
normalized_symbol = f"{symbol[0:3]}/{symbol[3:]}"
|
| 583 |
+
symbol = normalized_symbol
|
| 584 |
+
|
| 585 |
+
print(f"Getting positions for symbol: {symbol}, original input: {original_symbol}, normalized: {normalized_symbol}")
|
| 586 |
+
|
| 587 |
+
url = f"{self.base_url}/v2/positions"
|
| 588 |
+
headers = {
|
| 589 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 590 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
# First get all positions to handle different symbol formats
|
| 594 |
+
response = requests.get(url, headers=headers)
|
| 595 |
+
|
| 596 |
+
if response.status_code == 404:
|
| 597 |
+
# No positions found
|
| 598 |
+
print("No positions found (404 response)")
|
| 599 |
+
return {}
|
| 600 |
+
|
| 601 |
+
if response.status_code != 200:
|
| 602 |
+
print(f"Error getting positions: {response.text}")
|
| 603 |
+
return {}
|
| 604 |
+
|
| 605 |
+
positions_data = response.json()
|
| 606 |
+
|
| 607 |
+
# If nothing was returned or not a list
|
| 608 |
+
if not positions_data or not isinstance(positions_data, list):
|
| 609 |
+
print(f"No positions data returned or unexpected format: {positions_data}")
|
| 610 |
+
return {}
|
| 611 |
+
|
| 612 |
+
# If we're not looking for a specific symbol, return all positions
|
| 613 |
+
if not symbol:
|
| 614 |
+
return positions_data
|
| 615 |
+
|
| 616 |
+
# Find position that matches our symbol in any format
|
| 617 |
+
for position in positions_data:
|
| 618 |
+
pos_symbol = position.get("symbol", "")
|
| 619 |
+
print(f"Checking position with symbol: {pos_symbol}")
|
| 620 |
+
|
| 621 |
+
# Try different matching approaches to find BTC position
|
| 622 |
+
# 1. Direct match
|
| 623 |
+
if pos_symbol == symbol:
|
| 624 |
+
print(f"Direct symbol match found: {pos_symbol}")
|
| 625 |
+
return position
|
| 626 |
+
|
| 627 |
+
# 2. Normalized match (without slash)
|
| 628 |
+
if normalized_symbol and pos_symbol.replace('/', '') == normalized_symbol.replace('/', ''):
|
| 629 |
+
print(f"Normalized symbol match found: {pos_symbol} matches {normalized_symbol}")
|
| 630 |
+
return position
|
| 631 |
+
|
| 632 |
+
# 3. Match by asset name for crypto
|
| 633 |
+
if "BTC" in symbol.upper() and "BTC" in pos_symbol.upper():
|
| 634 |
+
print(f"BTC match found in position symbol: {pos_symbol}")
|
| 635 |
+
return position
|
| 636 |
+
|
| 637 |
+
# 4. Match by asset name for ETH
|
| 638 |
+
if "ETH" in symbol.upper() and "ETH" in pos_symbol.upper():
|
| 639 |
+
print(f"ETH match found in position symbol: {pos_symbol}")
|
| 640 |
+
return position
|
| 641 |
+
|
| 642 |
+
# If we got here, no matching position was found
|
| 643 |
+
print(f"No matching position found for {symbol} among: {[p.get('symbol', 'N/A') for p in positions_data]}")
|
| 644 |
+
return {}
|
| 645 |
+
|
| 646 |
+
except Exception as e:
|
| 647 |
+
print(f"Error getting positions: {str(e)}")
|
| 648 |
+
return {}
|
src/crypto_analysis/tools/technical_tools.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, Any, List, Optional
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytz # Import pytz for timezone handling
|
| 9 |
+
|
| 10 |
+
# Patch for pandas_ta NaN import issue
|
| 11 |
+
import sys
|
| 12 |
+
import importlib.util
|
| 13 |
+
import inspect
|
| 14 |
+
|
| 15 |
+
# Create a patched version of pandas_ta that won't try to import NaN from numpy
|
| 16 |
+
try:
|
| 17 |
+
import pandas_ta as ta
|
| 18 |
+
except ImportError as e:
|
| 19 |
+
if "cannot import name 'NaN' from 'numpy'" in str(e):
|
| 20 |
+
# Fix by monkeypatching numpy to provide NaN in expected location
|
| 21 |
+
np.NaN = np.nan
|
| 22 |
+
# Now import pandas_ta
|
| 23 |
+
import pandas_ta as ta
|
| 24 |
+
else:
|
| 25 |
+
raise
|
| 26 |
+
|
| 27 |
+
# Import OpenAI for LLM strategy interpretation
|
| 28 |
+
import openai
|
| 29 |
+
from dotenv import load_dotenv
|
| 30 |
+
load_dotenv()
|
| 31 |
+
|
| 32 |
+
from crewai.tools import BaseTool
|
| 33 |
+
from pydantic import Field
|
| 34 |
+
from alpaca.data.historical import CryptoHistoricalDataClient
|
| 35 |
+
from alpaca.data.requests import CryptoBarsRequest
|
| 36 |
+
from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
|
| 37 |
+
|
| 38 |
+
# Set up OpenAI API key
|
| 39 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 40 |
+
|
| 41 |
+
class IndicatorCalculator:
|
| 42 |
+
"""A modular class to calculate various technical indicators on price data."""
|
| 43 |
+
|
| 44 |
+
# Dictionary of available indicators with their descriptions
|
| 45 |
+
AVAILABLE_INDICATORS = {
|
| 46 |
+
"rsi": "Relative Strength Index - measures momentum by comparing recent gains to recent losses",
|
| 47 |
+
"bollinger_bands": "Bollinger Bands - measure volatility with upper and lower bands",
|
| 48 |
+
"macd": "Moving Average Convergence Divergence - momentum indicator showing relationship between two moving averages",
|
| 49 |
+
"adx": "Average Directional Index - measures trend strength",
|
| 50 |
+
"ema": "Exponential Moving Average - gives more weight to recent prices",
|
| 51 |
+
"sma": "Simple Moving Average - arithmetic mean of prices over a period",
|
| 52 |
+
"atr": "Average True Range - measures volatility",
|
| 53 |
+
"stochastic": "Stochastic Oscillator - momentum indicator comparing close price to price range",
|
| 54 |
+
"obv": "On-Balance Volume - relates volume to price change"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
def __init__(self):
|
| 58 |
+
"""Initialize the indicator calculator."""
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
def calculate_all_indicators(self, df: pd.DataFrame, params: Dict[str, Any] = None) -> pd.DataFrame:
|
| 62 |
+
"""
|
| 63 |
+
Calculate all available indicators on the price data
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
df: DataFrame with OHLCV data
|
| 67 |
+
params: Dictionary of parameters for indicators
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
DataFrame with added technical indicators
|
| 71 |
+
"""
|
| 72 |
+
if params is None:
|
| 73 |
+
params = {}
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
# Ensure we're working with a DataFrame with expected columns
|
| 77 |
+
required_columns = ['open', 'high', 'low', 'close', 'volume']
|
| 78 |
+
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 79 |
+
|
| 80 |
+
if missing_columns:
|
| 81 |
+
print(f"Missing columns in dataframe: {missing_columns}")
|
| 82 |
+
print(f"Available columns: {df.columns}")
|
| 83 |
+
return df
|
| 84 |
+
|
| 85 |
+
# Calculate RSI
|
| 86 |
+
rsi_length = params.get('rsi_length', 14)
|
| 87 |
+
df = self.calculate_rsi(df, length=rsi_length)
|
| 88 |
+
|
| 89 |
+
# Calculate Bollinger Bands
|
| 90 |
+
bb_length = params.get('bb_length', 20)
|
| 91 |
+
bb_std = params.get('bb_std', 2.0)
|
| 92 |
+
df = self.calculate_bollinger_bands(df, length=bb_length, std=bb_std)
|
| 93 |
+
|
| 94 |
+
# Calculate ADX
|
| 95 |
+
adx_length = params.get('adx_length', 14)
|
| 96 |
+
df = self.calculate_adx(df, length=adx_length)
|
| 97 |
+
|
| 98 |
+
# Calculate EMAs
|
| 99 |
+
ema_lengths = params.get('ema_lengths', [8, 21, 50, 200])
|
| 100 |
+
for length in ema_lengths:
|
| 101 |
+
df = self.calculate_ema(df, length=length)
|
| 102 |
+
|
| 103 |
+
# Calculate MACD
|
| 104 |
+
macd_fast = params.get('macd_fast', 12)
|
| 105 |
+
macd_slow = params.get('macd_slow', 26)
|
| 106 |
+
macd_signal = params.get('macd_signal', 9)
|
| 107 |
+
df = self.calculate_macd(df, fast=macd_fast, slow=macd_slow, signal=macd_signal)
|
| 108 |
+
|
| 109 |
+
# Calculate Stochastic Oscillator
|
| 110 |
+
stoch_k = params.get('stoch_k', 14)
|
| 111 |
+
stoch_d = params.get('stoch_d', 3)
|
| 112 |
+
df = self.calculate_stochastic(df, k=stoch_k, d=stoch_d)
|
| 113 |
+
|
| 114 |
+
# Calculate ATR
|
| 115 |
+
atr_length = params.get('atr_length', 14)
|
| 116 |
+
df = self.calculate_atr(df, length=atr_length)
|
| 117 |
+
|
| 118 |
+
# Calculate OBV
|
| 119 |
+
df = self.calculate_obv(df)
|
| 120 |
+
|
| 121 |
+
print(f"Calculated all technical indicators. Final data shape: {df.shape}")
|
| 122 |
+
|
| 123 |
+
return df
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Error calculating indicators: {e}")
|
| 127 |
+
import traceback
|
| 128 |
+
traceback.print_exc()
|
| 129 |
+
return df
|
| 130 |
+
|
| 131 |
+
def calculate_rsi(self, df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
|
| 132 |
+
"""Calculate RSI indicator."""
|
| 133 |
+
df[f'rsi_{length}'] = ta.rsi(df['close'], length=length)
|
| 134 |
+
return df
|
| 135 |
+
|
| 136 |
+
def calculate_bollinger_bands(self, df: pd.DataFrame, length: int = 20, std: float = 2.0) -> pd.DataFrame:
|
| 137 |
+
"""Calculate Bollinger Bands indicator."""
|
| 138 |
+
bbands = ta.bbands(df['close'], length=length, std=std)
|
| 139 |
+
df[f'bb_upper_{length}_{std}'] = bbands[f'BBU_{length}_{std}']
|
| 140 |
+
df[f'bb_middle_{length}_{std}'] = bbands[f'BBM_{length}_{std}']
|
| 141 |
+
df[f'bb_lower_{length}_{std}'] = bbands[f'BBL_{length}_{std}']
|
| 142 |
+
|
| 143 |
+
# Normalize Bollinger Bands position (0 = lower band, 1 = upper band)
|
| 144 |
+
df[f'bb_position_{length}_{std}'] = (df['close'] - df[f'bb_lower_{length}_{std}']) / (df[f'bb_upper_{length}_{std}'] - df[f'bb_lower_{length}_{std}'])
|
| 145 |
+
|
| 146 |
+
return df
|
| 147 |
+
|
| 148 |
+
def calculate_adx(self, df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
|
| 149 |
+
"""Calculate ADX indicator."""
|
| 150 |
+
adx = ta.adx(df['high'], df['low'], df['close'], length=length)
|
| 151 |
+
df[f'adx_{length}'] = adx[f'ADX_{length}']
|
| 152 |
+
df[f'di_plus_{length}'] = adx[f'DMP_{length}']
|
| 153 |
+
df[f'di_minus_{length}'] = adx[f'DMN_{length}']
|
| 154 |
+
return df
|
| 155 |
+
|
| 156 |
+
def calculate_ema(self, df: pd.DataFrame, length: int = 21) -> pd.DataFrame:
|
| 157 |
+
"""Calculate EMA indicator."""
|
| 158 |
+
df[f'ema_{length}'] = ta.ema(df['close'], length=length)
|
| 159 |
+
return df
|
| 160 |
+
|
| 161 |
+
def calculate_sma(self, df: pd.DataFrame, length: int = 21) -> pd.DataFrame:
|
| 162 |
+
"""Calculate SMA indicator."""
|
| 163 |
+
df[f'sma_{length}'] = ta.sma(df['close'], length=length)
|
| 164 |
+
return df
|
| 165 |
+
|
| 166 |
+
def calculate_macd(self, df: pd.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame:
|
| 167 |
+
"""Calculate MACD indicator."""
|
| 168 |
+
macd = ta.macd(df['close'], fast=fast, slow=slow, signal=signal)
|
| 169 |
+
df[f'macd_{fast}_{slow}_{signal}'] = macd[f'MACD_{fast}_{slow}_{signal}']
|
| 170 |
+
df[f'macd_signal_{fast}_{slow}_{signal}'] = macd[f'MACDs_{fast}_{slow}_{signal}']
|
| 171 |
+
df[f'macd_histogram_{fast}_{slow}_{signal}'] = macd[f'MACDh_{fast}_{slow}_{signal}']
|
| 172 |
+
return df
|
| 173 |
+
|
| 174 |
+
def calculate_stochastic(self, df: pd.DataFrame, k: int = 14, d: int = 3) -> pd.DataFrame:
|
| 175 |
+
"""Calculate Stochastic Oscillator."""
|
| 176 |
+
stoch = ta.stoch(df['high'], df['low'], df['close'], k=k, d=d)
|
| 177 |
+
df[f'stoch_k_{k}'] = stoch[f'STOCHk_{k}_{d}_3']
|
| 178 |
+
df[f'stoch_d_{k}_{d}'] = stoch[f'STOCHd_{k}_{d}_3']
|
| 179 |
+
return df
|
| 180 |
+
|
| 181 |
+
def calculate_atr(self, df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
|
| 182 |
+
"""Calculate Average True Range."""
|
| 183 |
+
df[f'atr_{length}'] = ta.atr(df['high'], df['low'], df['close'], length=length)
|
| 184 |
+
return df
|
| 185 |
+
|
| 186 |
+
def calculate_obv(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 187 |
+
"""Calculate On-Balance Volume."""
|
| 188 |
+
df['obv'] = ta.obv(df['close'], df['volume'])
|
| 189 |
+
return df
|
| 190 |
+
|
| 191 |
+
@classmethod
|
| 192 |
+
def get_available_indicators(cls) -> Dict[str, str]:
|
| 193 |
+
"""Return the dictionary of available indicators with descriptions."""
|
| 194 |
+
return cls.AVAILABLE_INDICATORS
|
| 195 |
+
|
| 196 |
+
class TechnicalAnalysisStrategy(BaseTool):
|
| 197 |
+
name: str = "Bitcoin Technical Analysis Strategy Tool"
|
| 198 |
+
description: str = "Fetches current Bitcoin technical indicators data from the market"
|
| 199 |
+
|
| 200 |
+
# Define all fields as proper Pydantic fields
|
| 201 |
+
api_key: Optional[str] = Field(default=None, description="Alpaca API key")
|
| 202 |
+
api_secret: Optional[str] = Field(default=None, description="Alpaca API secret")
|
| 203 |
+
client: Optional[Any] = Field(default=None, description="Alpaca client instance")
|
| 204 |
+
indicator_calculator: IndicatorCalculator = Field(default_factory=IndicatorCalculator, description="Indicator calculator instance")
|
| 205 |
+
|
| 206 |
+
# Add model_config to allow arbitrary types
|
| 207 |
+
model_config = {"arbitrary_types_allowed": True}
|
| 208 |
+
|
| 209 |
+
def __init__(self, **kwargs):
|
| 210 |
+
# Initialize the base class first
|
| 211 |
+
super().__init__(**kwargs)
|
| 212 |
+
|
| 213 |
+
# Set the API keys from environment variables if not provided directly
|
| 214 |
+
if not self.api_key:
|
| 215 |
+
self.api_key = os.getenv("ALPACA_API_KEY")
|
| 216 |
+
if not self.api_secret:
|
| 217 |
+
self.api_secret = os.getenv("ALPACA_API_SECRET")
|
| 218 |
+
|
| 219 |
+
print(f"Initializing Technical Analysis Strategy with API key: {'Present' if self.api_key else 'Missing'}")
|
| 220 |
+
|
| 221 |
+
# Initialize the Alpaca client if not already set
|
| 222 |
+
if not self.client and self.api_key and self.api_secret:
|
| 223 |
+
try:
|
| 224 |
+
self.client = CryptoHistoricalDataClient(api_key=self.api_key, secret_key=self.api_secret)
|
| 225 |
+
print("Successfully initialized Alpaca client")
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"Error initializing Alpaca client: {e}")
|
| 228 |
+
|
| 229 |
+
def _run(self) -> Dict[str, Any]:
|
| 230 |
+
"""
|
| 231 |
+
Fetch Bitcoin technical indicator data
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
Dictionary with all technical indicators data
|
| 235 |
+
"""
|
| 236 |
+
try:
|
| 237 |
+
print("Fetching Bitcoin technical indicator data")
|
| 238 |
+
|
| 239 |
+
# Make sure client is initialized
|
| 240 |
+
if not self.client:
|
| 241 |
+
print("Client not initialized, attempting to create one now")
|
| 242 |
+
self.client = CryptoHistoricalDataClient(api_key=self.api_key, secret_key=self.api_secret)
|
| 243 |
+
|
| 244 |
+
# Request extra periods to account for gaps in historical data
|
| 245 |
+
# Request 2x the minimum to ensure we get enough data points
|
| 246 |
+
min_required = 20 # Require fewer data points for basic analysis
|
| 247 |
+
lookback_periods = 100
|
| 248 |
+
actual_lookback = max(min_required, lookback_periods) * 3
|
| 249 |
+
timeframe_minutes = 5 # Default timeframe
|
| 250 |
+
|
| 251 |
+
# Fetch the Bitcoin price data for the specified timeframe
|
| 252 |
+
df = self._fetch_price_data(actual_lookback, timeframe_minutes)
|
| 253 |
+
|
| 254 |
+
if df is None:
|
| 255 |
+
print("No price data returned from fetch_price_data")
|
| 256 |
+
return {"error": "No price data available"}
|
| 257 |
+
|
| 258 |
+
print(f"Retrieved {len(df)} data points")
|
| 259 |
+
|
| 260 |
+
if len(df) < min_required:
|
| 261 |
+
print(f"Insufficient data points: {len(df)}, need at least {min_required}")
|
| 262 |
+
return {"error": f"Insufficient price data: only received {len(df)} data points"}
|
| 263 |
+
|
| 264 |
+
# Calculate all indicators
|
| 265 |
+
indicator_params = {
|
| 266 |
+
'bb_std': 2.0,
|
| 267 |
+
'rsi_length': 14,
|
| 268 |
+
'bb_length': 20
|
| 269 |
+
}
|
| 270 |
+
df = self.indicator_calculator.calculate_all_indicators(df, indicator_params)
|
| 271 |
+
|
| 272 |
+
# Get the latest data point with all indicators
|
| 273 |
+
latest_data = df.iloc[-1].to_dict()
|
| 274 |
+
|
| 275 |
+
# Prepare result dictionary
|
| 276 |
+
result = {}
|
| 277 |
+
|
| 278 |
+
# Add price data
|
| 279 |
+
result["price"] = float(latest_data.get('close', 0))
|
| 280 |
+
|
| 281 |
+
# Add all indicators
|
| 282 |
+
for key, value in latest_data.items():
|
| 283 |
+
if key not in ['open', 'high', 'low', 'close', 'volume', 'time']:
|
| 284 |
+
# Convert numpy values to Python native types
|
| 285 |
+
if pd.isna(value):
|
| 286 |
+
result[key] = None
|
| 287 |
+
else:
|
| 288 |
+
result[key] = float(value)
|
| 289 |
+
|
| 290 |
+
return result
|
| 291 |
+
|
| 292 |
+
except Exception as e:
|
| 293 |
+
print(f"Error fetching indicator data: {str(e)}")
|
| 294 |
+
return {"error": str(e)}
|
| 295 |
+
|
| 296 |
+
def _fetch_price_data(self, lookback_periods: int, timeframe_minutes: int = 5) -> pd.DataFrame:
|
| 297 |
+
"""
|
| 298 |
+
Fetch Bitcoin price data from Alpaca API
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
lookback_periods: Number of periods to fetch
|
| 302 |
+
timeframe_minutes: Timeframe in minutes
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
DataFrame with OHLCV data
|
| 306 |
+
"""
|
| 307 |
+
try:
|
| 308 |
+
# Calculate the start and end dates with timezone information
|
| 309 |
+
end = datetime.now(pytz.UTC) # Make end timezone-aware with UTC
|
| 310 |
+
start = end - timedelta(minutes=timeframe_minutes * lookback_periods)
|
| 311 |
+
|
| 312 |
+
print(f"Fetching price data from {start} to {end}")
|
| 313 |
+
|
| 314 |
+
# Create the request parameters
|
| 315 |
+
request_params = CryptoBarsRequest(
|
| 316 |
+
symbol_or_symbols=["BTC/USD"], # Use correct format with slash
|
| 317 |
+
timeframe=TimeFrame(timeframe_minutes, TimeFrameUnit.Minute), # Use specified timeframe
|
| 318 |
+
start=start,
|
| 319 |
+
end=end
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
print(f"Request parameters: {request_params}")
|
| 323 |
+
|
| 324 |
+
# Get the bars data
|
| 325 |
+
bars = self.client.get_crypto_bars(request_params)
|
| 326 |
+
|
| 327 |
+
if bars is None:
|
| 328 |
+
print("No bars data returned from API")
|
| 329 |
+
return None
|
| 330 |
+
|
| 331 |
+
# Convert to dataframe
|
| 332 |
+
df = bars.df.reset_index()
|
| 333 |
+
|
| 334 |
+
print(f"Raw data columns: {df.columns}")
|
| 335 |
+
print(f"Raw data shape: {df.shape}")
|
| 336 |
+
|
| 337 |
+
# Print first few rows for debugging
|
| 338 |
+
print(f"First few rows: {df.head(2)}")
|
| 339 |
+
|
| 340 |
+
# Ensure proper column names
|
| 341 |
+
if 'timestamp' in df.columns:
|
| 342 |
+
df = df.rename(columns={'timestamp': 'time'})
|
| 343 |
+
|
| 344 |
+
# Filter to only BTC/USD data
|
| 345 |
+
if 'symbol' in df.columns:
|
| 346 |
+
df = df[df['symbol'] == 'BTC/USD'].reset_index(drop=True)
|
| 347 |
+
df = df.drop(columns=['symbol'])
|
| 348 |
+
|
| 349 |
+
print(f"Processed data shape: {df.shape}")
|
| 350 |
+
return df
|
| 351 |
+
|
| 352 |
+
except Exception as e:
|
| 353 |
+
print(f"Error fetching price data with SDK: {e}")
|
| 354 |
+
print(f"API Key present: {'Yes' if self.api_key else 'No'}")
|
| 355 |
+
print(f"API Secret present: {'Yes' if self.api_secret else 'No'}")
|
| 356 |
+
|
| 357 |
+
# Try the direct REST API approach as fallback
|
| 358 |
+
try:
|
| 359 |
+
print("Attempting fallback to direct REST API call...")
|
| 360 |
+
return self._fetch_price_data_direct_api(lookback_periods, timeframe_minutes)
|
| 361 |
+
except Exception as e2:
|
| 362 |
+
print(f"Error in fallback API call: {e2}")
|
| 363 |
+
return None
|
| 364 |
+
|
| 365 |
+
def _fetch_price_data_direct_api(self, lookback_periods: int, timeframe_minutes: int = 5) -> pd.DataFrame:
|
| 366 |
+
"""
|
| 367 |
+
Fallback method to fetch Bitcoin price data using direct REST API calls
|
| 368 |
+
following the curl example format.
|
| 369 |
+
"""
|
| 370 |
+
# Calculate the start and end dates
|
| 371 |
+
end = datetime.now(pytz.UTC)
|
| 372 |
+
start = end - timedelta(minutes=timeframe_minutes * lookback_periods)
|
| 373 |
+
|
| 374 |
+
# Format dates for the API
|
| 375 |
+
start_str = start.strftime('%Y-%m-%dT%H:%M:%SZ')
|
| 376 |
+
end_str = end.strftime('%Y-%m-%dT%H:%M:%SZ')
|
| 377 |
+
|
| 378 |
+
# Endpoint for historical bars
|
| 379 |
+
url = f"https://data.alpaca.markets/v1beta3/crypto/us/bars"
|
| 380 |
+
|
| 381 |
+
# Query parameters
|
| 382 |
+
params = {
|
| 383 |
+
"symbols": "BTC/USD",
|
| 384 |
+
"timeframe": f"{timeframe_minutes}Min",
|
| 385 |
+
"start": start_str,
|
| 386 |
+
"end": end_str,
|
| 387 |
+
"limit": lookback_periods
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
# Headers with authentication
|
| 391 |
+
headers = {
|
| 392 |
+
"Apca-Api-Key-Id": self.api_key,
|
| 393 |
+
"Apca-Api-Secret-Key": self.api_secret
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
print(f"Making direct API call to: {url}")
|
| 397 |
+
print(f"With params: {params}")
|
| 398 |
+
|
| 399 |
+
# Make the request
|
| 400 |
+
response = requests.get(url, params=params, headers=headers)
|
| 401 |
+
|
| 402 |
+
if response.status_code != 200:
|
| 403 |
+
print(f"API Error: {response.status_code} - {response.text}")
|
| 404 |
+
return None
|
| 405 |
+
|
| 406 |
+
# Parse response
|
| 407 |
+
data = response.json()
|
| 408 |
+
print(f"API Response: {json.dumps(data)[:300]}...")
|
| 409 |
+
|
| 410 |
+
# Extract the bars
|
| 411 |
+
if 'bars' not in data or 'BTC/USD' not in data['bars']:
|
| 412 |
+
print("No bar data found in response")
|
| 413 |
+
return None
|
| 414 |
+
|
| 415 |
+
bars_data = data['bars']['BTC/USD']
|
| 416 |
+
|
| 417 |
+
# Create DataFrame
|
| 418 |
+
df = pd.DataFrame(bars_data)
|
| 419 |
+
|
| 420 |
+
# Rename columns to match expected format
|
| 421 |
+
if 't' in df.columns:
|
| 422 |
+
df = df.rename(columns={
|
| 423 |
+
't': 'time',
|
| 424 |
+
'o': 'open',
|
| 425 |
+
'h': 'high',
|
| 426 |
+
'l': 'low',
|
| 427 |
+
'c': 'close',
|
| 428 |
+
'v': 'volume'
|
| 429 |
+
})
|
| 430 |
+
|
| 431 |
+
# Convert timestamp to datetime
|
| 432 |
+
if 'time' in df.columns:
|
| 433 |
+
df['time'] = pd.to_datetime(df['time'])
|
| 434 |
+
|
| 435 |
+
print(f"Processed API data shape: {df.shape}")
|
| 436 |
+
return df
|
src/crypto_analysis/tools/yahoo_tools.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Dict, Any, List, Optional
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import yfinance as yf
|
| 6 |
+
from crewai.tools import BaseTool
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
import requests
|
| 10 |
+
|
| 11 |
+
# Set up logging
|
| 12 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 13 |
+
logger = logging.getLogger("yahoo_tools")
|
| 14 |
+
|
| 15 |
+
class YahooBitcoinDataTool(BaseTool):
|
| 16 |
+
name: str = "Yahoo Finance Bitcoin Data Tool"
|
| 17 |
+
description: str = "Fetches Bitcoin price data from Yahoo Finance as an alternative data source"
|
| 18 |
+
max_retries: int = 3
|
| 19 |
+
backoff_factor: float = 2.0
|
| 20 |
+
timeout: int = 30
|
| 21 |
+
cache_duration_minutes: int = 15
|
| 22 |
+
cached_data: Dict[str, Dict[str, Any]] = {}
|
| 23 |
+
last_cache_time: Dict[str, datetime] = {}
|
| 24 |
+
|
| 25 |
+
def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0, timeout: int = 30, cache_duration_minutes: int = 15):
|
| 26 |
+
"""
|
| 27 |
+
Initialize the tool with retry parameters
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
max_retries: Maximum number of retry attempts (default: 3)
|
| 31 |
+
backoff_factor: Exponential backoff factor between retries (default: 2.0)
|
| 32 |
+
timeout: Request timeout in seconds (default: 30)
|
| 33 |
+
cache_duration_minutes: How long to cache results (default: 15 minutes)
|
| 34 |
+
"""
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.max_retries = max_retries
|
| 37 |
+
self.backoff_factor = backoff_factor
|
| 38 |
+
self.timeout = timeout
|
| 39 |
+
self.cache_duration_minutes = cache_duration_minutes
|
| 40 |
+
self.cached_data = {} # Cache by period and interval
|
| 41 |
+
self.last_cache_time = {} # Cache timestamps by period and interval
|
| 42 |
+
|
| 43 |
+
def _check_cache_valid(self, period: str, interval: str) -> bool:
|
| 44 |
+
"""
|
| 45 |
+
Check if the cached data is still valid
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
period: The time period key
|
| 49 |
+
interval: The interval key
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
True if cache is valid, False otherwise
|
| 53 |
+
"""
|
| 54 |
+
cache_key = f"{period}_{interval}"
|
| 55 |
+
|
| 56 |
+
if cache_key not in self.cached_data or cache_key not in self.last_cache_time:
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
cache_age = datetime.now() - self.last_cache_time[cache_key]
|
| 60 |
+
return cache_age.total_seconds() < (self.cache_duration_minutes * 60)
|
| 61 |
+
|
| 62 |
+
def _run(self, period: str = "1mo", interval: str = "1d") -> Dict[str, Any]:
|
| 63 |
+
"""
|
| 64 |
+
Fetch Bitcoin price data from Yahoo Finance
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
period: Time period to fetch data for (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
|
| 68 |
+
interval: Time interval between data points (1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo)
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
Dictionary with OHLCV data and metadata
|
| 72 |
+
"""
|
| 73 |
+
# Use cached data if available and valid
|
| 74 |
+
cache_key = f"{period}_{interval}"
|
| 75 |
+
if self._check_cache_valid(period, interval):
|
| 76 |
+
logger.info(f"Using cached Bitcoin data for period={period}, interval={interval}")
|
| 77 |
+
return self.cached_data[cache_key]
|
| 78 |
+
|
| 79 |
+
logger.info(f"Fetching Bitcoin data for period={period}, interval={interval}")
|
| 80 |
+
|
| 81 |
+
# BTC-USD ticker from Yahoo Finance
|
| 82 |
+
ticker = "BTC-USD"
|
| 83 |
+
|
| 84 |
+
# Alternative tickers to try if primary fails
|
| 85 |
+
alternative_tickers = ["BTC-USD", "BTCUSD=X", "BTC=F"]
|
| 86 |
+
|
| 87 |
+
data_dict = None
|
| 88 |
+
|
| 89 |
+
# Try the primary ticker with retry logic
|
| 90 |
+
for attempt in range(self.max_retries):
|
| 91 |
+
try:
|
| 92 |
+
logger.info(f"Fetching {ticker} data (attempt {attempt + 1}/{self.max_retries})")
|
| 93 |
+
|
| 94 |
+
# Get the data
|
| 95 |
+
data = yf.Ticker(ticker)
|
| 96 |
+
df = data.history(period=period, interval=interval, timeout=self.timeout)
|
| 97 |
+
|
| 98 |
+
# Check if we have valid data
|
| 99 |
+
if df.empty:
|
| 100 |
+
error_msg = f"{ticker}: possibly delisted; no price data found (period={period})"
|
| 101 |
+
logger.warning(error_msg)
|
| 102 |
+
# Don't retry with the same ticker, break to try alternatives
|
| 103 |
+
break
|
| 104 |
+
|
| 105 |
+
# Process the data
|
| 106 |
+
df = df.reset_index()
|
| 107 |
+
|
| 108 |
+
# Ensure proper column names
|
| 109 |
+
if 'Date' in df.columns:
|
| 110 |
+
df = df.rename(columns={'Date': 'time'})
|
| 111 |
+
elif 'Datetime' in df.columns:
|
| 112 |
+
df = df.rename(columns={'Datetime': 'time'})
|
| 113 |
+
|
| 114 |
+
# Standardize column names to lowercase
|
| 115 |
+
df.columns = [col.lower() for col in df.columns]
|
| 116 |
+
|
| 117 |
+
# Convert numpy types to Python native types for JSON serialization
|
| 118 |
+
for col in df.columns:
|
| 119 |
+
if col != 'time':
|
| 120 |
+
df[col] = df[col].apply(lambda x: x.item() if hasattr(x, 'item') else x)
|
| 121 |
+
|
| 122 |
+
# Metadata about Bitcoin from Yahoo Finance
|
| 123 |
+
info = data.info
|
| 124 |
+
|
| 125 |
+
# Extract key information
|
| 126 |
+
market_cap = info.get('marketCap', None)
|
| 127 |
+
volume_24h = info.get('volume24Hr', None)
|
| 128 |
+
circulating_supply = info.get('circulatingSupply', None)
|
| 129 |
+
|
| 130 |
+
# Convert to dictionary for JSON serialization
|
| 131 |
+
data_dict = {
|
| 132 |
+
"dataframe": df.to_dict(orient='records'),
|
| 133 |
+
"last_price": float(df['close'].iloc[-1]) if not df.empty else None,
|
| 134 |
+
"time_period": period,
|
| 135 |
+
"interval": interval,
|
| 136 |
+
"ticker": ticker,
|
| 137 |
+
"metadata": {
|
| 138 |
+
"market_cap": market_cap,
|
| 139 |
+
"volume_24h": volume_24h,
|
| 140 |
+
"circulating_supply": circulating_supply,
|
| 141 |
+
"last_updated": datetime.now().isoformat()
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Cache the data
|
| 146 |
+
self.cached_data[cache_key] = data_dict
|
| 147 |
+
self.last_cache_time[cache_key] = datetime.now()
|
| 148 |
+
logger.info(f"Cached Bitcoin data for period={period}, interval={interval}")
|
| 149 |
+
|
| 150 |
+
return data_dict
|
| 151 |
+
|
| 152 |
+
except requests.exceptions.Timeout as e:
|
| 153 |
+
wait_time = self.backoff_factor ** attempt
|
| 154 |
+
logger.warning(f"Timeout error for {ticker}: {str(e)}. Retrying in {wait_time:.1f} seconds...")
|
| 155 |
+
if attempt < self.max_retries - 1:
|
| 156 |
+
time.sleep(wait_time)
|
| 157 |
+
else:
|
| 158 |
+
logger.error(f"Max retries reached for {ticker}")
|
| 159 |
+
# Don't return error yet, try alternative tickers
|
| 160 |
+
break
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logger.error(f"Error fetching data for {ticker}: {str(e)}")
|
| 164 |
+
# Don't return error yet, try alternative tickers
|
| 165 |
+
break
|
| 166 |
+
|
| 167 |
+
# If we get here, the primary ticker failed - try alternatives
|
| 168 |
+
if data_dict is None:
|
| 169 |
+
for alt_ticker in alternative_tickers:
|
| 170 |
+
# Skip the one we already tried
|
| 171 |
+
if alt_ticker == ticker:
|
| 172 |
+
continue
|
| 173 |
+
|
| 174 |
+
logger.info(f"Trying alternative ticker: {alt_ticker}")
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
# Get the data with the alternative ticker
|
| 178 |
+
data = yf.Ticker(alt_ticker)
|
| 179 |
+
df = data.history(period=period, interval=interval, timeout=self.timeout)
|
| 180 |
+
|
| 181 |
+
# Check if we have valid data
|
| 182 |
+
if df.empty:
|
| 183 |
+
logger.warning(f"{alt_ticker}: possibly delisted; no price data found (period={period})")
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
# Process the data
|
| 187 |
+
df = df.reset_index()
|
| 188 |
+
|
| 189 |
+
# Ensure proper column names
|
| 190 |
+
if 'Date' in df.columns:
|
| 191 |
+
df = df.rename(columns={'Date': 'time'})
|
| 192 |
+
elif 'Datetime' in df.columns:
|
| 193 |
+
df = df.rename(columns={'Datetime': 'time'})
|
| 194 |
+
|
| 195 |
+
# Standardize column names to lowercase
|
| 196 |
+
df.columns = [col.lower() for col in df.columns]
|
| 197 |
+
|
| 198 |
+
# Convert numpy types to Python native types for JSON serialization
|
| 199 |
+
for col in df.columns:
|
| 200 |
+
if col != 'time':
|
| 201 |
+
df[col] = df[col].apply(lambda x: x.item() if hasattr(x, 'item') else x)
|
| 202 |
+
|
| 203 |
+
# Metadata about Bitcoin from Yahoo Finance
|
| 204 |
+
info = data.info
|
| 205 |
+
|
| 206 |
+
# Extract key information
|
| 207 |
+
market_cap = info.get('marketCap', None)
|
| 208 |
+
volume_24h = info.get('volume24Hr', None)
|
| 209 |
+
circulating_supply = info.get('circulatingSupply', None)
|
| 210 |
+
|
| 211 |
+
# Convert to dictionary for JSON serialization
|
| 212 |
+
data_dict = {
|
| 213 |
+
"dataframe": df.to_dict(orient='records'),
|
| 214 |
+
"last_price": float(df['close'].iloc[-1]) if not df.empty else None,
|
| 215 |
+
"time_period": period,
|
| 216 |
+
"interval": interval,
|
| 217 |
+
"ticker": alt_ticker,
|
| 218 |
+
"metadata": {
|
| 219 |
+
"market_cap": market_cap,
|
| 220 |
+
"volume_24h": volume_24h,
|
| 221 |
+
"circulating_supply": circulating_supply,
|
| 222 |
+
"last_updated": datetime.now().isoformat()
|
| 223 |
+
},
|
| 224 |
+
"note": f"Used alternative ticker {alt_ticker} because primary ticker failed"
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
# Cache the data
|
| 228 |
+
self.cached_data[cache_key] = data_dict
|
| 229 |
+
self.last_cache_time[cache_key] = datetime.now()
|
| 230 |
+
logger.info(f"Cached Bitcoin data for period={period}, interval={interval} using alternate ticker {alt_ticker}")
|
| 231 |
+
|
| 232 |
+
return data_dict
|
| 233 |
+
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Error fetching data for alternative ticker {alt_ticker}: {str(e)}")
|
| 236 |
+
continue
|
| 237 |
+
|
| 238 |
+
# If we get here, all tickers failed
|
| 239 |
+
error_msg = f"Failed to fetch Bitcoin data for period={period}, interval={interval} with all available tickers"
|
| 240 |
+
logger.error(error_msg)
|
| 241 |
+
return {
|
| 242 |
+
"error": error_msg,
|
| 243 |
+
"time_period": period,
|
| 244 |
+
"interval": interval,
|
| 245 |
+
"tickers_tried": alternative_tickers
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class YahooCryptoMarketTool(BaseTool):
|
| 250 |
+
name: str = "Yahoo Finance Crypto Market Tool"
|
| 251 |
+
description: str = "Fetches data about the broader cryptocurrency market for contextual analysis"
|
| 252 |
+
max_retries: int = 3
|
| 253 |
+
backoff_factor: float = 2.0
|
| 254 |
+
timeout: int = 30
|
| 255 |
+
cache_duration_minutes: int = 30
|
| 256 |
+
cached_data: Dict[str, Any] = None
|
| 257 |
+
last_cache_time: datetime = None
|
| 258 |
+
|
| 259 |
+
def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0, timeout: int = 30, cache_duration_minutes: int = 30):
|
| 260 |
+
"""
|
| 261 |
+
Initialize the tool with retry parameters
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
max_retries: Maximum number of retry attempts (default: 3)
|
| 265 |
+
backoff_factor: Exponential backoff factor between retries (default: 2.0)
|
| 266 |
+
timeout: Request timeout in seconds (default: 30)
|
| 267 |
+
cache_duration_minutes: How long to cache results (default: 30 minutes)
|
| 268 |
+
"""
|
| 269 |
+
super().__init__()
|
| 270 |
+
self.max_retries = max_retries
|
| 271 |
+
self.backoff_factor = backoff_factor
|
| 272 |
+
self.timeout = timeout
|
| 273 |
+
self.cache_duration_minutes = cache_duration_minutes
|
| 274 |
+
self.cached_data = None
|
| 275 |
+
self.last_cache_time = None
|
| 276 |
+
|
| 277 |
+
def _check_cache_valid(self) -> bool:
|
| 278 |
+
"""
|
| 279 |
+
Check if the cached data is still valid
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
True if cache is valid, False otherwise
|
| 283 |
+
"""
|
| 284 |
+
if self.cached_data is None or self.last_cache_time is None:
|
| 285 |
+
return False
|
| 286 |
+
|
| 287 |
+
cache_age = datetime.now() - self.last_cache_time
|
| 288 |
+
return cache_age.total_seconds() < (self.cache_duration_minutes * 60)
|
| 289 |
+
|
| 290 |
+
def _get_ticker_data_with_retries(self, ticker: str) -> Dict[str, Any]:
|
| 291 |
+
"""
|
| 292 |
+
Get ticker data with retry logic
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
ticker: The ticker symbol to fetch
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
Dictionary with ticker data or error
|
| 299 |
+
"""
|
| 300 |
+
for attempt in range(self.max_retries):
|
| 301 |
+
try:
|
| 302 |
+
logger.info(f"Fetching data for {ticker} (attempt {attempt + 1}/{self.max_retries})")
|
| 303 |
+
data = yf.Ticker(ticker)
|
| 304 |
+
|
| 305 |
+
# Set a timeout for the history call
|
| 306 |
+
hist = data.history(period="5d", timeout=self.timeout)
|
| 307 |
+
info = data.info
|
| 308 |
+
|
| 309 |
+
# Verify we have data
|
| 310 |
+
if hist.empty:
|
| 311 |
+
logger.warning(f"{ticker}: possibly delisted; no price data found (period=5d)")
|
| 312 |
+
return {
|
| 313 |
+
"ticker": ticker,
|
| 314 |
+
"error": f"{ticker}: possibly delisted; no price data found (period=5d)"
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
# Successfully got data, return it
|
| 318 |
+
return {
|
| 319 |
+
"data": data,
|
| 320 |
+
"hist": hist,
|
| 321 |
+
"info": info
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
except requests.exceptions.Timeout as e:
|
| 325 |
+
wait_time = self.backoff_factor ** attempt
|
| 326 |
+
logger.warning(f"Timeout error for {ticker}: {str(e)}. Retrying in {wait_time:.1f} seconds...")
|
| 327 |
+
if attempt < self.max_retries - 1:
|
| 328 |
+
time.sleep(wait_time)
|
| 329 |
+
else:
|
| 330 |
+
logger.error(f"Max retries reached for {ticker}")
|
| 331 |
+
return {
|
| 332 |
+
"ticker": ticker,
|
| 333 |
+
"error": f"Timeout error after {self.max_retries} attempts: {str(e)}"
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
except Exception as e:
|
| 337 |
+
logger.error(f"Error fetching data for {ticker}: {str(e)}")
|
| 338 |
+
return {
|
| 339 |
+
"ticker": ticker,
|
| 340 |
+
"error": str(e)
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
return {
|
| 344 |
+
"ticker": ticker,
|
| 345 |
+
"error": "Unknown error during retry attempts"
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
def _run(self, top_n: int = 10) -> Dict[str, Any]:
|
| 349 |
+
"""
|
| 350 |
+
Fetch data about the top cryptocurrencies in the market
|
| 351 |
+
|
| 352 |
+
Args:
|
| 353 |
+
top_n: Number of top cryptocurrencies to fetch data for
|
| 354 |
+
|
| 355 |
+
Returns:
|
| 356 |
+
Dictionary with market data and trends
|
| 357 |
+
"""
|
| 358 |
+
# Check if we have valid cached data
|
| 359 |
+
if self._check_cache_valid():
|
| 360 |
+
logger.info("Using cached cryptocurrency market data")
|
| 361 |
+
return self.cached_data
|
| 362 |
+
|
| 363 |
+
logger.info(f"Fetching data for top {top_n} cryptocurrencies")
|
| 364 |
+
|
| 365 |
+
try:
|
| 366 |
+
# Common crypto tickers to check
|
| 367 |
+
tickers = [
|
| 368 |
+
"BTC-USD", # Bitcoin
|
| 369 |
+
"ETH-USD", # Ethereum
|
| 370 |
+
"XRP-USD", # Ripple
|
| 371 |
+
"SOL-USD", # Solana
|
| 372 |
+
"ADA-USD", # Cardano
|
| 373 |
+
"AVAX-USD", # Avalanche
|
| 374 |
+
"DOT-USD", # Polkadot
|
| 375 |
+
"DOGE-USD", # Dogecoin
|
| 376 |
+
"LINK-USD", # Chainlink
|
| 377 |
+
"MATIC-USD" # Polygon
|
| 378 |
+
]
|
| 379 |
+
|
| 380 |
+
# Limit to requested number
|
| 381 |
+
tickers = tickers[:min(top_n, len(tickers))]
|
| 382 |
+
|
| 383 |
+
results = []
|
| 384 |
+
market_cap_sum = 0
|
| 385 |
+
btc_dominance = 0
|
| 386 |
+
btc_market_cap = 0
|
| 387 |
+
success_count = 0
|
| 388 |
+
error_count = 0
|
| 389 |
+
|
| 390 |
+
# Fetch data for each ticker
|
| 391 |
+
for ticker in tickers:
|
| 392 |
+
ticker_data = self._get_ticker_data_with_retries(ticker)
|
| 393 |
+
|
| 394 |
+
if "error" in ticker_data:
|
| 395 |
+
# Handle error case - add basic info with error message
|
| 396 |
+
error_count += 1
|
| 397 |
+
results.append({
|
| 398 |
+
"ticker": ticker,
|
| 399 |
+
"name": ticker.split('-')[0],
|
| 400 |
+
"error": ticker_data["error"],
|
| 401 |
+
"data_available": False
|
| 402 |
+
})
|
| 403 |
+
continue
|
| 404 |
+
|
| 405 |
+
# Extract data
|
| 406 |
+
data = ticker_data["data"]
|
| 407 |
+
hist = ticker_data["hist"]
|
| 408 |
+
info = ticker_data["info"]
|
| 409 |
+
|
| 410 |
+
success_count += 1
|
| 411 |
+
|
| 412 |
+
if not hist.empty:
|
| 413 |
+
current_price = hist['Close'].iloc[-1]
|
| 414 |
+
day_change = ((current_price / hist['Close'].iloc[-2]) - 1) * 100 if len(hist) > 1 else 0
|
| 415 |
+
week_change = ((current_price / hist['Close'].iloc[0]) - 1) * 100 if len(hist) > 4 else day_change
|
| 416 |
+
|
| 417 |
+
market_cap = info.get('marketCap', 0)
|
| 418 |
+
market_cap_sum += market_cap if market_cap else 0
|
| 419 |
+
|
| 420 |
+
# Store BTC market cap for dominance calculation
|
| 421 |
+
if ticker == "BTC-USD":
|
| 422 |
+
btc_market_cap = market_cap if market_cap else 0
|
| 423 |
+
|
| 424 |
+
# Convert numpy types to Python native types for JSON serialization
|
| 425 |
+
if hasattr(current_price, 'item'):
|
| 426 |
+
current_price = current_price.item()
|
| 427 |
+
if hasattr(day_change, 'item'):
|
| 428 |
+
day_change = day_change.item()
|
| 429 |
+
if hasattr(week_change, 'item'):
|
| 430 |
+
week_change = week_change.item()
|
| 431 |
+
|
| 432 |
+
results.append({
|
| 433 |
+
"ticker": ticker,
|
| 434 |
+
"name": info.get('shortName', ticker.split('-')[0]),
|
| 435 |
+
"current_price": current_price,
|
| 436 |
+
"market_cap": market_cap,
|
| 437 |
+
"volume_24h": info.get('volume24Hr', None),
|
| 438 |
+
"day_change_percent": day_change,
|
| 439 |
+
"week_change_percent": week_change,
|
| 440 |
+
"data_available": True
|
| 441 |
+
})
|
| 442 |
+
|
| 443 |
+
# Calculate BTC dominance
|
| 444 |
+
if market_cap_sum > 0 and btc_market_cap > 0:
|
| 445 |
+
btc_dominance = (btc_market_cap / market_cap_sum) * 100
|
| 446 |
+
|
| 447 |
+
# Overall market trends - only count assets with data
|
| 448 |
+
valid_results = [r for r in results if r.get("data_available", False)]
|
| 449 |
+
market_trend = "bullish" if sum(r.get('day_change_percent', 0) for r in valid_results) > 0 else "bearish"
|
| 450 |
+
|
| 451 |
+
# Create response
|
| 452 |
+
response = {
|
| 453 |
+
"cryptocurrencies": results,
|
| 454 |
+
"market_summary": {
|
| 455 |
+
"total_market_cap": market_cap_sum,
|
| 456 |
+
"btc_dominance": btc_dominance,
|
| 457 |
+
"market_trend": market_trend,
|
| 458 |
+
"timestamp": datetime.now().isoformat(),
|
| 459 |
+
"success_count": success_count,
|
| 460 |
+
"error_count": error_count,
|
| 461 |
+
"total_count": len(tickers)
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
# Cache the result if we got at least some data
|
| 466 |
+
if success_count > 0:
|
| 467 |
+
self.cached_data = response
|
| 468 |
+
self.last_cache_time = datetime.now()
|
| 469 |
+
logger.info(f"Cached cryptocurrency market data (success: {success_count}, errors: {error_count})")
|
| 470 |
+
|
| 471 |
+
return response
|
| 472 |
+
|
| 473 |
+
except Exception as e:
|
| 474 |
+
logger.error(f"Error in YahooCryptoMarketTool: {str(e)}")
|
| 475 |
+
return {
|
| 476 |
+
"error": str(e),
|
| 477 |
+
"market_summary": {
|
| 478 |
+
"market_trend": "unknown",
|
| 479 |
+
"timestamp": datetime.now().isoformat(),
|
| 480 |
+
"error": str(e)
|
| 481 |
+
},
|
| 482 |
+
"cryptocurrencies": []
|
| 483 |
+
}
|
src/crypto_analysis/utils/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shared utility functions for the crypto analysis package
|
| 3 |
+
"""
|
src/crypto_analysis/utils/api_helpers.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API helper utilities for reliable data fetching with retry logic
|
| 3 |
+
"""
|
| 4 |
+
import time
|
| 5 |
+
import logging
|
| 6 |
+
import functools
|
| 7 |
+
import numpy as np
|
| 8 |
+
from typing import Any, Dict, Optional, Callable, TypeVar, cast, Union
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import requests
|
| 11 |
+
from tenacity import (
|
| 12 |
+
retry,
|
| 13 |
+
stop_after_attempt,
|
| 14 |
+
wait_exponential,
|
| 15 |
+
retry_if_exception_type,
|
| 16 |
+
RetryError
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Set up logging
|
| 20 |
+
logger = logging.getLogger("api_helpers")
|
| 21 |
+
|
| 22 |
+
# Type variable for return type of functions
|
| 23 |
+
T = TypeVar('T')
|
| 24 |
+
|
| 25 |
+
def validate_dataframe(df: pd.DataFrame, required_columns: list, min_rows: int = 1) -> bool:
|
| 26 |
+
"""
|
| 27 |
+
Validate that a pandas DataFrame meets minimum requirements
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
df: DataFrame to validate
|
| 31 |
+
required_columns: List of column names that must be present
|
| 32 |
+
min_rows: Minimum number of rows required
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
True if valid, False otherwise
|
| 36 |
+
"""
|
| 37 |
+
# Check if DataFrame is empty
|
| 38 |
+
if df is None or df.empty or len(df) < min_rows:
|
| 39 |
+
logger.warning(f"DataFrame validation failed: empty or too few rows (expected {min_rows}, got {0 if df is None or df.empty else len(df)})")
|
| 40 |
+
return False
|
| 41 |
+
|
| 42 |
+
# Check for required columns
|
| 43 |
+
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 44 |
+
if missing_columns:
|
| 45 |
+
logger.warning(f"DataFrame validation failed: missing columns {missing_columns}")
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
return True
|
| 49 |
+
|
| 50 |
+
def convert_numpy_types(obj: Any) -> Any:
|
| 51 |
+
"""
|
| 52 |
+
Convert numpy types to native Python types for JSON serialization
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
obj: Object that might contain numpy types
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Object with numpy types converted to Python types
|
| 59 |
+
"""
|
| 60 |
+
if isinstance(obj, np.integer):
|
| 61 |
+
return int(obj)
|
| 62 |
+
elif isinstance(obj, np.floating):
|
| 63 |
+
return float(obj)
|
| 64 |
+
elif isinstance(obj, np.ndarray):
|
| 65 |
+
return obj.tolist()
|
| 66 |
+
elif isinstance(obj, pd.DataFrame):
|
| 67 |
+
return obj.to_dict(orient='records')
|
| 68 |
+
elif isinstance(obj, pd.Series):
|
| 69 |
+
return obj.to_dict()
|
| 70 |
+
elif isinstance(obj, dict):
|
| 71 |
+
return {k: convert_numpy_types(v) for k, v in obj.items()}
|
| 72 |
+
elif isinstance(obj, list):
|
| 73 |
+
return [convert_numpy_types(item) for item in obj]
|
| 74 |
+
else:
|
| 75 |
+
return obj
|
| 76 |
+
|
| 77 |
+
def safe_api_call(
|
| 78 |
+
func: Callable[..., T],
|
| 79 |
+
max_retries: int = 3,
|
| 80 |
+
backoff_factor: float = 2.0,
|
| 81 |
+
timeout: int = 30,
|
| 82 |
+
expected_exceptions: tuple = (requests.exceptions.RequestException,),
|
| 83 |
+
validation_func: Optional[Callable[[T], bool]] = None
|
| 84 |
+
) -> Callable[..., Dict[str, Any]]:
|
| 85 |
+
"""
|
| 86 |
+
Decorator for safely making API calls with retries and error handling
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
func: Function to wrap
|
| 90 |
+
max_retries: Maximum number of retry attempts
|
| 91 |
+
backoff_factor: Exponential backoff factor
|
| 92 |
+
timeout: Request timeout in seconds
|
| 93 |
+
expected_exceptions: Exceptions to retry on
|
| 94 |
+
validation_func: Optional function to validate the response
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Wrapped function that returns a dict with either data or error
|
| 98 |
+
"""
|
| 99 |
+
@functools.wraps(func)
|
| 100 |
+
def wrapper(*args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 101 |
+
"""
|
| 102 |
+
Wrapper function that adds retry logic and error handling
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Dictionary with either successful data or error information
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
# Add the timeout parameter if it's a keyword argument in the original function
|
| 109 |
+
if 'timeout' in kwargs:
|
| 110 |
+
# Only override if not explicitly provided
|
| 111 |
+
if kwargs['timeout'] is None:
|
| 112 |
+
kwargs['timeout'] = timeout
|
| 113 |
+
|
| 114 |
+
# Apply the retry decorator dynamically
|
| 115 |
+
retried_func = retry(
|
| 116 |
+
stop=stop_after_attempt(max_retries),
|
| 117 |
+
wait=wait_exponential(multiplier=1, min=backoff_factor, max=backoff_factor * 10),
|
| 118 |
+
retry=retry_if_exception_type(expected_exceptions),
|
| 119 |
+
reraise=True
|
| 120 |
+
)(func)
|
| 121 |
+
|
| 122 |
+
# Call the function with retries
|
| 123 |
+
result = retried_func(*args, **kwargs)
|
| 124 |
+
|
| 125 |
+
# Validate result if validation function is provided
|
| 126 |
+
if validation_func and not validation_func(result):
|
| 127 |
+
return {
|
| 128 |
+
"success": False,
|
| 129 |
+
"error": "Data validation failed",
|
| 130 |
+
"data": None
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# Convert numpy types for JSON serialization
|
| 134 |
+
result = convert_numpy_types(result)
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"success": True,
|
| 138 |
+
"data": result,
|
| 139 |
+
"error": None
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
except RetryError as e:
|
| 143 |
+
# This means we exceeded max retries
|
| 144 |
+
original_error = e.__cause__
|
| 145 |
+
logger.error(f"Max retries exceeded in {func.__name__}: {str(original_error)}")
|
| 146 |
+
return {
|
| 147 |
+
"success": False,
|
| 148 |
+
"error": f"Max retries exceeded: {str(original_error)}",
|
| 149 |
+
"data": None
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Error in {func.__name__}: {str(e)}", exc_info=True)
|
| 154 |
+
return {
|
| 155 |
+
"success": False,
|
| 156 |
+
"error": str(e),
|
| 157 |
+
"data": None
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
return wrapper
|
| 161 |
+
|
| 162 |
+
def with_exponential_backoff(
|
| 163 |
+
max_retries: int = 3,
|
| 164 |
+
backoff_factor: float = 2.0,
|
| 165 |
+
expected_exceptions: tuple = (Exception,)
|
| 166 |
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
| 167 |
+
"""
|
| 168 |
+
Decorator for adding exponential backoff retry logic to any function
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
max_retries: Maximum number of retry attempts
|
| 172 |
+
backoff_factor: Exponential backoff factor
|
| 173 |
+
expected_exceptions: Exceptions to retry on
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
Decorator function
|
| 177 |
+
"""
|
| 178 |
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
| 179 |
+
@functools.wraps(func)
|
| 180 |
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
| 181 |
+
"""
|
| 182 |
+
Wrapper function that adds retry logic
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
Result of the original function
|
| 186 |
+
"""
|
| 187 |
+
for attempt in range(max_retries):
|
| 188 |
+
try:
|
| 189 |
+
return func(*args, **kwargs)
|
| 190 |
+
except expected_exceptions as e:
|
| 191 |
+
if attempt == max_retries - 1:
|
| 192 |
+
# Last attempt, re-raise the exception
|
| 193 |
+
raise
|
| 194 |
+
|
| 195 |
+
# Calculate wait time with exponential backoff
|
| 196 |
+
wait_time = backoff_factor ** attempt
|
| 197 |
+
logger.warning(f"Attempt {attempt + 1}/{max_retries} failed: {str(e)}. Retrying in {wait_time:.1f} seconds...")
|
| 198 |
+
time.sleep(wait_time)
|
| 199 |
+
|
| 200 |
+
# This should not be reached, but return a sensible default
|
| 201 |
+
return cast(T, None)
|
| 202 |
+
|
| 203 |
+
return wrapper
|
| 204 |
+
|
| 205 |
+
return decorator
|
| 206 |
+
|
| 207 |
+
def handle_api_result(
|
| 208 |
+
result: Dict[str, Any],
|
| 209 |
+
default_value: T,
|
| 210 |
+
error_prefix: str = "API Error"
|
| 211 |
+
) -> Union[T, Dict[str, Any]]:
|
| 212 |
+
"""
|
| 213 |
+
Handle the result from a safe_api_call wrapped function
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
result: The result dictionary from safe_api_call
|
| 217 |
+
default_value: Default value to return if the API call failed
|
| 218 |
+
error_prefix: Prefix for error message
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
Either the successful data or an error dictionary
|
| 222 |
+
"""
|
| 223 |
+
if result.get("success", False):
|
| 224 |
+
return result.get("data", default_value)
|
| 225 |
+
else:
|
| 226 |
+
error_msg = f"{error_prefix}: {result.get('error', 'Unknown error')}"
|
| 227 |
+
logger.error(error_msg)
|
| 228 |
+
return {
|
| 229 |
+
"error": error_msg,
|
| 230 |
+
"data": default_value
|
| 231 |
+
}
|
src/stock_analysis/__init__.py
ADDED
|
File without changes
|
src/stock_analysis/config/agents.yaml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
financial_analyst:
|
| 2 |
+
role: >
|
| 3 |
+
The Best Financial Analyst
|
| 4 |
+
goal: >
|
| 5 |
+
Impress all customers with your financial data and market trends analysis
|
| 6 |
+
backstory: >
|
| 7 |
+
The most seasoned financial analyst with lots of expertise in stock market analysis and investment
|
| 8 |
+
strategies that is working for a super important customer.
|
| 9 |
+
|
| 10 |
+
research_analyst:
|
| 11 |
+
role: >
|
| 12 |
+
Staff Research Analyst
|
| 13 |
+
goal: >
|
| 14 |
+
Being the best at gathering, interpreting data and amazing
|
| 15 |
+
your customer with it
|
| 16 |
+
backstory: >
|
| 17 |
+
Known as the BEST research analyst, you're skilled in sifting through news, company announcements,
|
| 18 |
+
and market sentiments. Now you're working on a super important customer.
|
| 19 |
+
|
| 20 |
+
investment_advisor:
|
| 21 |
+
role: >
|
| 22 |
+
Private Investment Advisor
|
| 23 |
+
goal: >
|
| 24 |
+
Impress your customers with full analyses over stocks
|
| 25 |
+
and complete investment recommendations
|
| 26 |
+
backstory: >
|
| 27 |
+
You're the most experienced investment advisor
|
| 28 |
+
and you combine various analytical insights to formulate
|
| 29 |
+
strategic investment advice. You are now working for
|
| 30 |
+
a super important customer you need to impress.
|
src/stock_analysis/config/tasks.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
financial_analysis:
|
| 2 |
+
description: >
|
| 3 |
+
Conduct a thorough analysis of {company_stock}'s stock financial health and market performance. This includes examining key financial metrics such as
|
| 4 |
+
P/E ratio, EPS growth, revenue trends, and debt-to-equity ratio. Also, analyze the stock's performance in comparison
|
| 5 |
+
to its industry peers and overall market trends.
|
| 6 |
+
|
| 7 |
+
expected_output: >
|
| 8 |
+
The final report must expand on the summary provided but now
|
| 9 |
+
including a clear assessment of the stock's financial standing, its strengths and weaknesses,
|
| 10 |
+
and how it fares against its competitors in the current market scenario.
|
| 11 |
+
Make sure to use the most recent data possible.
|
| 12 |
+
|
| 13 |
+
research:
|
| 14 |
+
description: >
|
| 15 |
+
Collect and summarize recent news articles, press
|
| 16 |
+
releases, and market analyses related to the {company_stock} stock and its industry.
|
| 17 |
+
Pay special attention to any significant events, market sentiments, and analysts' opinions.
|
| 18 |
+
Also include upcoming events like earnings and others.
|
| 19 |
+
|
| 20 |
+
expected_output: >
|
| 21 |
+
A report that includes a comprehensive summary of the latest news,
|
| 22 |
+
any notable shifts in market sentiment, and potential impacts on the stock. Also make sure to return the stock ticker as {company_stock}.
|
| 23 |
+
Make sure to use the most recent data as possible.
|
| 24 |
+
|
| 25 |
+
filings_analysis:
|
| 26 |
+
description: >
|
| 27 |
+
Analyze the latest 10-Q and 10-K filings from EDGAR for the stock {company_stock} in question.
|
| 28 |
+
Focus on key sections like Management's Discussion and analysis, financial statements, insider trading activity,
|
| 29 |
+
and any disclosed risks. Extract relevant data and insights that could influence
|
| 30 |
+
the stock's future performance.
|
| 31 |
+
|
| 32 |
+
expected_output: >
|
| 33 |
+
Final answer must be an expanded report that now also highlights significant findings
|
| 34 |
+
from these filings including any red flags or positive indicators for your customer.
|
| 35 |
+
|
| 36 |
+
recommend:
|
| 37 |
+
description: >
|
| 38 |
+
Review and synthesize the analyses provided by the
|
| 39 |
+
Financial Analyst and the Research Analyst.
|
| 40 |
+
Combine these insights to form a comprehensive
|
| 41 |
+
investment recommendation. You MUST Consider all aspects, including financial
|
| 42 |
+
health, market sentiment, and qualitative data from
|
| 43 |
+
EDGAR filings.
|
| 44 |
+
|
| 45 |
+
Make sure to include a section that shows insider
|
| 46 |
+
trading activity, and upcoming events like earnings.
|
| 47 |
+
|
| 48 |
+
expected_output: >
|
| 49 |
+
Your final answer MUST be a recommendation for your customer. It should be a full super detailed report, providing a
|
| 50 |
+
clear investment stance and strategy with supporting evidence.
|
| 51 |
+
Make it pretty and well formatted for your customer.
|
src/stock_analysis/crew.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from crewai import Agent, Crew, Process, Task
|
| 3 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 4 |
+
|
| 5 |
+
from tools.calculator_tool import CalculatorTool
|
| 6 |
+
from tools.sec_tools import SEC10KTool, SEC10QTool
|
| 7 |
+
|
| 8 |
+
from crewai_tools import WebsiteSearchTool, ScrapeWebsiteTool, TXTSearchTool
|
| 9 |
+
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
from langchain.llms import Ollama
|
| 14 |
+
llm = Ollama(model="llama3.1")
|
| 15 |
+
|
| 16 |
+
@CrewBase
|
| 17 |
+
class StockAnalysisCrew:
|
| 18 |
+
agents_config = 'config/agents.yaml'
|
| 19 |
+
tasks_config = 'config/tasks.yaml'
|
| 20 |
+
|
| 21 |
+
@agent
|
| 22 |
+
def financial_agent(self) -> Agent:
|
| 23 |
+
return Agent(
|
| 24 |
+
config=self.agents_config['financial_analyst'],
|
| 25 |
+
verbose=True,
|
| 26 |
+
llm=llm,
|
| 27 |
+
tools=[
|
| 28 |
+
ScrapeWebsiteTool(),
|
| 29 |
+
WebsiteSearchTool(),
|
| 30 |
+
CalculatorTool(),
|
| 31 |
+
SEC10QTool("AMZN"),
|
| 32 |
+
SEC10KTool("AMZN"),
|
| 33 |
+
]
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
@task
|
| 37 |
+
def financial_analysis(self) -> Task:
|
| 38 |
+
return Task(
|
| 39 |
+
config=self.tasks_config['financial_analysis'],
|
| 40 |
+
agent=self.financial_agent(),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@agent
|
| 45 |
+
def research_analyst_agent(self) -> Agent:
|
| 46 |
+
return Agent(
|
| 47 |
+
config=self.agents_config['research_analyst'],
|
| 48 |
+
verbose=True,
|
| 49 |
+
llm=llm,
|
| 50 |
+
tools=[
|
| 51 |
+
ScrapeWebsiteTool(),
|
| 52 |
+
# WebsiteSearchTool(),
|
| 53 |
+
SEC10QTool("AMZN"),
|
| 54 |
+
SEC10KTool("AMZN"),
|
| 55 |
+
]
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
@task
|
| 59 |
+
def research(self) -> Task:
|
| 60 |
+
return Task(
|
| 61 |
+
config=self.tasks_config['research'],
|
| 62 |
+
agent=self.research_analyst_agent(),
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
@agent
|
| 66 |
+
def financial_analyst_agent(self) -> Agent:
|
| 67 |
+
return Agent(
|
| 68 |
+
config=self.agents_config['financial_analyst'],
|
| 69 |
+
verbose=True,
|
| 70 |
+
llm=llm,
|
| 71 |
+
tools=[
|
| 72 |
+
ScrapeWebsiteTool(),
|
| 73 |
+
WebsiteSearchTool(),
|
| 74 |
+
CalculatorTool(),
|
| 75 |
+
SEC10QTool(),
|
| 76 |
+
SEC10KTool(),
|
| 77 |
+
]
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
@task
|
| 81 |
+
def financial_analysis(self) -> Task:
|
| 82 |
+
return Task(
|
| 83 |
+
config=self.tasks_config['financial_analysis'],
|
| 84 |
+
agent=self.financial_analyst_agent(),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
@task
|
| 88 |
+
def filings_analysis(self) -> Task:
|
| 89 |
+
return Task(
|
| 90 |
+
config=self.tasks_config['filings_analysis'],
|
| 91 |
+
agent=self.financial_analyst_agent(),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
@agent
|
| 95 |
+
def investment_advisor_agent(self) -> Agent:
|
| 96 |
+
return Agent(
|
| 97 |
+
config=self.agents_config['investment_advisor'],
|
| 98 |
+
verbose=True,
|
| 99 |
+
llm=llm,
|
| 100 |
+
tools=[
|
| 101 |
+
ScrapeWebsiteTool(),
|
| 102 |
+
WebsiteSearchTool(),
|
| 103 |
+
CalculatorTool(),
|
| 104 |
+
]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
@task
|
| 108 |
+
def recommend(self) -> Task:
|
| 109 |
+
return Task(
|
| 110 |
+
config=self.tasks_config['recommend'],
|
| 111 |
+
agent=self.investment_advisor_agent(),
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@crew
|
| 116 |
+
def crew(self) -> Crew:
|
| 117 |
+
"""Creates the Stock Analysis"""
|
| 118 |
+
return Crew(
|
| 119 |
+
agents=self.agents,
|
| 120 |
+
tasks=self.tasks,
|
| 121 |
+
process=Process.sequential,
|
| 122 |
+
verbose=True,
|
| 123 |
+
)
|
src/stock_analysis/main.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from crew import StockAnalysisCrew
|
| 3 |
+
|
| 4 |
+
def run():
|
| 5 |
+
inputs = {
|
| 6 |
+
'query': 'What is the company you want to analyze?',
|
| 7 |
+
'company_stock': 'AMZN',
|
| 8 |
+
}
|
| 9 |
+
return StockAnalysisCrew().crew().kickoff(inputs=inputs)
|
| 10 |
+
|
| 11 |
+
def train():
|
| 12 |
+
"""
|
| 13 |
+
Train the crew for a given number of iterations.
|
| 14 |
+
"""
|
| 15 |
+
inputs = {
|
| 16 |
+
'query': 'What is last years revenue',
|
| 17 |
+
'company_stock': 'AMZN',
|
| 18 |
+
}
|
| 19 |
+
try:
|
| 20 |
+
StockAnalysisCrew().crew().train(n_iterations=int(sys.argv[1]), inputs=inputs)
|
| 21 |
+
|
| 22 |
+
except Exception as e:
|
| 23 |
+
raise Exception(f"An error occurred while training the crew: {e}")
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
print("## Welcome to Stock Analysis Crew")
|
| 27 |
+
print('-------------------------------')
|
| 28 |
+
result = run()
|
| 29 |
+
print("\n\n########################")
|
| 30 |
+
print("## Here is the Report")
|
| 31 |
+
print("########################\n")
|
| 32 |
+
print(result)
|
src/stock_analysis/tools/__init__.py
ADDED
|
File without changes
|
src/stock_analysis/tools/calculator_tool.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai_tools import BaseTool
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class CalculatorTool(BaseTool):
|
| 5 |
+
name: str = "Calculator tool"
|
| 6 |
+
description: str = (
|
| 7 |
+
"Useful to perform any mathematical calculations, like sum, minus, multiplication, division, etc. The input to this tool should be a mathematical expression, a couple examples are `200*7` or `5000/2*10."
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
def _run(self, operation: str) -> int:
|
| 11 |
+
# Implementation goes here
|
| 12 |
+
return eval(operation)
|
src/stock_analysis/tools/sec_tools.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Any, Optional, Type
|
| 3 |
+
from pydantic.v1 import BaseModel, Field
|
| 4 |
+
from crewai_tools import RagTool
|
| 5 |
+
from sec_api import QueryApi # Make sure to have sec_api installed
|
| 6 |
+
from embedchain.models.data_type import DataType
|
| 7 |
+
import requests
|
| 8 |
+
import html2text
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
class FixedSEC10KToolSchema(BaseModel):
|
| 12 |
+
"""Input for SEC10KTool."""
|
| 13 |
+
search_query: str = Field(
|
| 14 |
+
...,
|
| 15 |
+
description="Mandatory query you would like to search from the 10-K report",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
class SEC10KToolSchema(FixedSEC10KToolSchema):
|
| 19 |
+
"""Input for SEC10KTool."""
|
| 20 |
+
stock_name: str = Field(
|
| 21 |
+
..., description="Mandatory valid stock name you would like to search"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
class SEC10KTool(RagTool):
|
| 25 |
+
name: str = "Search in the specified 10-K form"
|
| 26 |
+
description: str = "A tool that can be used to semantic search a query from a 10-K form for a specified company."
|
| 27 |
+
args_schema: Type[BaseModel] = SEC10KToolSchema
|
| 28 |
+
|
| 29 |
+
def __init__(self, stock_name: Optional[str] = None, **kwargs):
|
| 30 |
+
print("enter init")
|
| 31 |
+
# exit()
|
| 32 |
+
super().__init__(**kwargs)
|
| 33 |
+
if stock_name is not None:
|
| 34 |
+
content = self.get_10k_url_content(stock_name)
|
| 35 |
+
if content:
|
| 36 |
+
self.add(content)
|
| 37 |
+
# print("exit init")
|
| 38 |
+
# exit()
|
| 39 |
+
self.description = f"A tool that can be used to semantic search a query from {stock_name}'s latest 10-K SEC form's content as a txt file."
|
| 40 |
+
self.args_schema = FixedSEC10KToolSchema
|
| 41 |
+
self._generate_description()
|
| 42 |
+
|
| 43 |
+
def get_10k_url_content(self, stock_name: str) -> Optional[str]:
|
| 44 |
+
"""Fetches the URL content as txt of the latest 10-K form for the given stock name."""
|
| 45 |
+
try:
|
| 46 |
+
queryApi = QueryApi(api_key=os.environ['SEC_API_API_KEY'])
|
| 47 |
+
query = {
|
| 48 |
+
"query": {
|
| 49 |
+
"query_string": {
|
| 50 |
+
"query": f"ticker:{stock_name} AND formType:\"10-K\""
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"from": "0",
|
| 54 |
+
"size": "1",
|
| 55 |
+
"sort": [{ "filedAt": { "order": "desc" }}]
|
| 56 |
+
}
|
| 57 |
+
filings = queryApi.get_filings(query)['filings']
|
| 58 |
+
if len(filings) == 0:
|
| 59 |
+
print("No filings found for this stock.")
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
url = filings[0]['linkToFilingDetails']
|
| 63 |
+
|
| 64 |
+
headers = {
|
| 65 |
+
"User-Agent": "crewai.com bisan@crewai.com",
|
| 66 |
+
"Accept-Encoding": "gzip, deflate",
|
| 67 |
+
"Host": "www.sec.gov"
|
| 68 |
+
}
|
| 69 |
+
response = requests.get(url, headers=headers)
|
| 70 |
+
response.raise_for_status()
|
| 71 |
+
h = html2text.HTML2Text()
|
| 72 |
+
h.ignore_links = False
|
| 73 |
+
text = h.handle(response.content.decode("utf-8"))
|
| 74 |
+
|
| 75 |
+
text = re.sub(r"[^a-zA-Z$0-9\s\n]", "", text)
|
| 76 |
+
return text
|
| 77 |
+
except requests.exceptions.HTTPError as e:
|
| 78 |
+
print(f"HTTP error occurred: {e}")
|
| 79 |
+
return None
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"Error fetching 10-K URL: {e}")
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
def add(self, *args: Any, **kwargs: Any) -> None:
|
| 85 |
+
kwargs["data_type"] = DataType.TEXT
|
| 86 |
+
super().add(*args, **kwargs)
|
| 87 |
+
|
| 88 |
+
def _run(self, search_query: str, **kwargs: Any) -> Any:
|
| 89 |
+
return super()._run(query=search_query, **kwargs)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class FixedSEC10QToolSchema(BaseModel):
|
| 93 |
+
"""Input for SEC10QTool."""
|
| 94 |
+
search_query: str = Field(
|
| 95 |
+
...,
|
| 96 |
+
description="Mandatory query you would like to search from the 10-Q report",
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
class SEC10QToolSchema(FixedSEC10QToolSchema):
|
| 100 |
+
"""Input for SEC10QTool."""
|
| 101 |
+
stock_name: str = Field(
|
| 102 |
+
..., description="Mandatory valid stock name you would like to search"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
class SEC10QTool(RagTool):
|
| 106 |
+
name: str = "Search in the specified 10-Q form"
|
| 107 |
+
description: str = "A tool that can be used to semantic search a query from a 10-Q form for a specified company."
|
| 108 |
+
args_schema: Type[BaseModel] = SEC10QToolSchema
|
| 109 |
+
|
| 110 |
+
def __init__(self, stock_name: Optional[str] = None, **kwargs):
|
| 111 |
+
print("enter init")
|
| 112 |
+
# exit()
|
| 113 |
+
super().__init__(**kwargs)
|
| 114 |
+
if stock_name is not None:
|
| 115 |
+
content = self.get_10q_url_content(stock_name)
|
| 116 |
+
if content:
|
| 117 |
+
self.add(content)
|
| 118 |
+
self.description = f"A tool that can be used to semantic search a query from {stock_name}'s latest 10-Q SEC form's content as a txt file."
|
| 119 |
+
self.args_schema = FixedSEC10QToolSchema
|
| 120 |
+
self._generate_description()
|
| 121 |
+
|
| 122 |
+
def get_10q_url_content(self, stock_name: str) -> Optional[str]:
|
| 123 |
+
"""Fetches the URL content as txt of the latest 10-Q form for the given stock name."""
|
| 124 |
+
try:
|
| 125 |
+
queryApi = QueryApi(api_key=os.environ['SEC_API_API_KEY'])
|
| 126 |
+
query = {
|
| 127 |
+
"query": {
|
| 128 |
+
"query_string": {
|
| 129 |
+
"query": f"ticker:{stock_name} AND formType:\"10-Q\""
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"from": "0",
|
| 133 |
+
"size": "1",
|
| 134 |
+
"sort": [{ "filedAt": { "order": "desc" }}]
|
| 135 |
+
}
|
| 136 |
+
filings = queryApi.get_filings(query)['filings']
|
| 137 |
+
if len(filings) == 0:
|
| 138 |
+
print("No filings found for this stock.")
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
url = filings[0]['linkToFilingDetails']
|
| 142 |
+
|
| 143 |
+
headers = {
|
| 144 |
+
"User-Agent": "crewai.com bisan@crewai.com",
|
| 145 |
+
"Accept-Encoding": "gzip, deflate",
|
| 146 |
+
"Host": "www.sec.gov"
|
| 147 |
+
}
|
| 148 |
+
response = requests.get(url, headers=headers)
|
| 149 |
+
response.raise_for_status() # Raise an exception for HTTP errors
|
| 150 |
+
h = html2text.HTML2Text()
|
| 151 |
+
h.ignore_links = False
|
| 152 |
+
text = h.handle(response.content.decode("utf-8"))
|
| 153 |
+
|
| 154 |
+
# Removing all non-English words, dollar signs, numbers, and newlines from text
|
| 155 |
+
text = re.sub(r"[^a-zA-Z$0-9\s\n]", "", text)
|
| 156 |
+
return text
|
| 157 |
+
except requests.exceptions.HTTPError as e:
|
| 158 |
+
print(f"HTTP error occurred: {e}")
|
| 159 |
+
return None
|
| 160 |
+
except Exception as e:
|
| 161 |
+
print(f"Error fetching 10-Q URL: {e}")
|
| 162 |
+
return None
|
| 163 |
+
|
| 164 |
+
def add(self, *args: Any, **kwargs: Any) -> None:
|
| 165 |
+
kwargs["data_type"] = DataType.TEXT
|
| 166 |
+
super().add(*args, **kwargs)
|
| 167 |
+
|
| 168 |
+
def _run(self, search_query: str, **kwargs: Any) -> Any:
|
| 169 |
+
return super()._run(query=search_query, **kwargs)
|
| 170 |
+
|
src/ui/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CrypticAI UI Package
|
| 3 |
+
|
| 4 |
+
This package contains the user interface components for the CrypticAI Bitcoin
|
| 5 |
+
Trading Dashboard.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
__version__ = "0.1.0"
|
src/ui/app.py
ADDED
|
@@ -0,0 +1,1657 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import json
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import threading
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
import requests
|
| 12 |
+
import sqlite3
|
| 13 |
+
from typing import Dict, Any, List, Optional
|
| 14 |
+
import openai
|
| 15 |
+
|
| 16 |
+
# Add project root to path for imports
|
| 17 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
| 18 |
+
|
| 19 |
+
# Import necessary modules
|
| 20 |
+
from src.crypto_analysis.tools.technical_tools import TechnicalAnalysisStrategy, IndicatorCalculator
|
| 21 |
+
from src.crypto_analysis.tools.order_tools import AlpacaCryptoOrderTool
|
| 22 |
+
from src.crypto_analysis.tools.bitcoin_tools import YahooBitcoinDataTool
|
| 23 |
+
from src.crypto_analysis.tools.yahoo_tools import YahooCryptoMarketTool
|
| 24 |
+
from src.crypto_analysis.crew import BitcoinAnalysisCrew
|
| 25 |
+
|
| 26 |
+
# Load environment variables
|
| 27 |
+
from dotenv import load_dotenv
|
| 28 |
+
load_dotenv()
|
| 29 |
+
|
| 30 |
+
# Initialize the database
|
| 31 |
+
DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../data/cryptic.db'))
|
| 32 |
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
| 33 |
+
|
| 34 |
+
def init_database():
|
| 35 |
+
"""Initialize the SQLite database with necessary tables"""
|
| 36 |
+
conn = sqlite3.connect(DB_PATH)
|
| 37 |
+
cursor = conn.cursor()
|
| 38 |
+
|
| 39 |
+
# Create strategies table
|
| 40 |
+
cursor.execute('''
|
| 41 |
+
CREATE TABLE IF NOT EXISTS strategies (
|
| 42 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 43 |
+
name TEXT NOT NULL UNIQUE,
|
| 44 |
+
description TEXT,
|
| 45 |
+
strategy_text TEXT NOT NULL,
|
| 46 |
+
parameters TEXT NOT NULL,
|
| 47 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 48 |
+
)
|
| 49 |
+
''')
|
| 50 |
+
|
| 51 |
+
# Create transactions table
|
| 52 |
+
cursor.execute('''
|
| 53 |
+
CREATE TABLE IF NOT EXISTS transactions (
|
| 54 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 55 |
+
symbol TEXT NOT NULL,
|
| 56 |
+
action TEXT NOT NULL,
|
| 57 |
+
quantity REAL NOT NULL,
|
| 58 |
+
price REAL NOT NULL,
|
| 59 |
+
status TEXT NOT NULL,
|
| 60 |
+
allocation_percentage INTEGER,
|
| 61 |
+
order_id TEXT,
|
| 62 |
+
strategy_id INTEGER,
|
| 63 |
+
reasoning TEXT,
|
| 64 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 65 |
+
FOREIGN KEY (strategy_id) REFERENCES strategies (id)
|
| 66 |
+
)
|
| 67 |
+
''')
|
| 68 |
+
|
| 69 |
+
# Create analysis_results table
|
| 70 |
+
cursor.execute('''
|
| 71 |
+
CREATE TABLE IF NOT EXISTS analysis_results (
|
| 72 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 73 |
+
signal TEXT NOT NULL,
|
| 74 |
+
confidence INTEGER,
|
| 75 |
+
allocation_percentage INTEGER,
|
| 76 |
+
reasoning TEXT,
|
| 77 |
+
indicator_values TEXT,
|
| 78 |
+
strategy_id INTEGER,
|
| 79 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 80 |
+
FOREIGN KEY (strategy_id) REFERENCES strategies (id)
|
| 81 |
+
)
|
| 82 |
+
''')
|
| 83 |
+
|
| 84 |
+
conn.commit()
|
| 85 |
+
conn.close()
|
| 86 |
+
|
| 87 |
+
# Initialize the database when module is loaded
|
| 88 |
+
init_database()
|
| 89 |
+
|
| 90 |
+
# Global variables for strategy parameters
|
| 91 |
+
strategy_params = {
|
| 92 |
+
"timeframe_minutes": 60,
|
| 93 |
+
"max_allocation_percentage": 50,
|
| 94 |
+
"strategy_text": None
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Store analysis results and orders
|
| 98 |
+
analysis_results = []
|
| 99 |
+
orders_history = []
|
| 100 |
+
active_trades = []
|
| 101 |
+
|
| 102 |
+
# Flag to control the background thread
|
| 103 |
+
running = False
|
| 104 |
+
background_thread = None
|
| 105 |
+
|
| 106 |
+
# Function to get available strategies from the database
|
| 107 |
+
def get_saved_strategies():
|
| 108 |
+
conn = sqlite3.connect(DB_PATH)
|
| 109 |
+
cursor = conn.cursor()
|
| 110 |
+
cursor.execute("SELECT id, name, description FROM strategies")
|
| 111 |
+
strategies = [{"id": row[0], "name": row[1], "description": row[2]} for row in cursor.fetchall()]
|
| 112 |
+
conn.close()
|
| 113 |
+
return strategies
|
| 114 |
+
|
| 115 |
+
# Function to get a specific strategy by ID
|
| 116 |
+
def get_strategy_by_id(strategy_id):
|
| 117 |
+
conn = sqlite3.connect(DB_PATH)
|
| 118 |
+
cursor = conn.cursor()
|
| 119 |
+
cursor.execute("SELECT id, name, description, strategy_text, parameters FROM strategies WHERE id = ?", (strategy_id,))
|
| 120 |
+
row = cursor.fetchone()
|
| 121 |
+
conn.close()
|
| 122 |
+
|
| 123 |
+
if row:
|
| 124 |
+
return {
|
| 125 |
+
"id": row[0],
|
| 126 |
+
"name": row[1],
|
| 127 |
+
"description": row[2],
|
| 128 |
+
"strategy_text": row[3],
|
| 129 |
+
"parameters": json.loads(row[4])
|
| 130 |
+
}
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
# Function to save a strategy to the database
|
| 134 |
+
def save_strategy(name, description, strategy_text, parameters):
|
| 135 |
+
try:
|
| 136 |
+
conn = sqlite3.connect(DB_PATH)
|
| 137 |
+
cursor = conn.cursor()
|
| 138 |
+
|
| 139 |
+
# Check if strategy name already exists
|
| 140 |
+
cursor.execute("SELECT id FROM strategies WHERE name = ?", (name,))
|
| 141 |
+
existing = cursor.fetchone()
|
| 142 |
+
|
| 143 |
+
if existing:
|
| 144 |
+
# Update existing strategy
|
| 145 |
+
cursor.execute(
|
| 146 |
+
"UPDATE strategies SET description = ?, strategy_text = ?, parameters = ? WHERE name = ?",
|
| 147 |
+
(description, strategy_text, json.dumps(parameters), name)
|
| 148 |
+
)
|
| 149 |
+
else:
|
| 150 |
+
# Insert new strategy
|
| 151 |
+
cursor.execute(
|
| 152 |
+
"INSERT INTO strategies (name, description, strategy_text, parameters) VALUES (?, ?, ?, ?)",
|
| 153 |
+
(name, strategy_text, strategy_text, json.dumps(parameters))
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
conn.commit()
|
| 157 |
+
conn.close()
|
| 158 |
+
return True, "Strategy saved successfully"
|
| 159 |
+
except Exception as e:
|
| 160 |
+
return False, f"Error saving strategy: {str(e)}"
|
| 161 |
+
|
| 162 |
+
# Function to save analysis result to the database
|
| 163 |
+
def save_analysis_result(result, strategy_id=None):
|
| 164 |
+
try:
|
| 165 |
+
conn = sqlite3.connect(DB_PATH)
|
| 166 |
+
cursor = conn.cursor()
|
| 167 |
+
|
| 168 |
+
# Extract values from result
|
| 169 |
+
signal = result.get("signal", "unknown")
|
| 170 |
+
confidence = result.get("confidence", 0)
|
| 171 |
+
allocation_percentage = result.get("allocation_percentage", 0)
|
| 172 |
+
reasoning = result.get("reasoning", "")
|
| 173 |
+
|
| 174 |
+
# Convert technical indicators to JSON string
|
| 175 |
+
indicator_values = json.dumps(result.get("technical_indicators", {}))
|
| 176 |
+
|
| 177 |
+
# Insert into database
|
| 178 |
+
cursor.execute(
|
| 179 |
+
"INSERT INTO analysis_results (signal, confidence, allocation_percentage, reasoning, indicator_values, strategy_id) VALUES (?, ?, ?, ?, ?, ?)",
|
| 180 |
+
(signal, confidence, allocation_percentage, reasoning, indicator_values, strategy_id)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
conn.commit()
|
| 184 |
+
conn.close()
|
| 185 |
+
return True
|
| 186 |
+
except Exception as e:
|
| 187 |
+
print(f"Error saving analysis result: {e}")
|
| 188 |
+
return False
|
| 189 |
+
|
| 190 |
+
# Function to save transaction to the database
|
| 191 |
+
def save_transaction(order_data, strategy_id=None):
|
| 192 |
+
try:
|
| 193 |
+
conn = sqlite3.connect(DB_PATH)
|
| 194 |
+
cursor = conn.cursor()
|
| 195 |
+
|
| 196 |
+
# Extract values from order data
|
| 197 |
+
symbol = order_data.get("symbol", "BTC/USD")
|
| 198 |
+
action = order_data.get("action", "unknown")
|
| 199 |
+
quantity = order_data.get("quantity", 0)
|
| 200 |
+
price = order_data.get("price", 0)
|
| 201 |
+
status = order_data.get("status", "unknown")
|
| 202 |
+
allocation_percentage = order_data.get("allocation_percentage", 0)
|
| 203 |
+
order_id = order_data.get("order_id", "")
|
| 204 |
+
reasoning = order_data.get("reasoning", "")
|
| 205 |
+
|
| 206 |
+
# Insert into database
|
| 207 |
+
cursor.execute(
|
| 208 |
+
"INSERT INTO transactions (symbol, action, quantity, price, status, allocation_percentage, order_id, strategy_id, reasoning) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
| 209 |
+
(symbol, action, quantity, price, status, allocation_percentage, order_id, strategy_id, reasoning)
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
conn.commit()
|
| 213 |
+
conn.close()
|
| 214 |
+
return True
|
| 215 |
+
except Exception as e:
|
| 216 |
+
print(f"Error saving transaction: {e}")
|
| 217 |
+
return False
|
| 218 |
+
|
| 219 |
+
# Function to get recent analysis results from the database
|
| 220 |
+
def get_recent_analysis_results(limit=10):
|
| 221 |
+
conn = sqlite3.connect(DB_PATH)
|
| 222 |
+
cursor = conn.cursor()
|
| 223 |
+
cursor.execute("""
|
| 224 |
+
SELECT ar.id, ar.signal, ar.confidence, ar.allocation_percentage, ar.reasoning,
|
| 225 |
+
ar.timestamp, s.name as strategy_name
|
| 226 |
+
FROM analysis_results ar
|
| 227 |
+
LEFT JOIN strategies s ON ar.strategy_id = s.id
|
| 228 |
+
ORDER BY ar.timestamp DESC
|
| 229 |
+
LIMIT ?
|
| 230 |
+
""", (limit,))
|
| 231 |
+
|
| 232 |
+
results = []
|
| 233 |
+
for row in cursor.fetchall():
|
| 234 |
+
results.append({
|
| 235 |
+
"id": row[0],
|
| 236 |
+
"signal": row[1],
|
| 237 |
+
"confidence": row[2],
|
| 238 |
+
"allocation_percentage": row[3],
|
| 239 |
+
"reasoning": row[4],
|
| 240 |
+
"timestamp": row[5],
|
| 241 |
+
"strategy_name": row[6] or "Default Strategy"
|
| 242 |
+
})
|
| 243 |
+
|
| 244 |
+
conn.close()
|
| 245 |
+
return results
|
| 246 |
+
|
| 247 |
+
# Function to get recent transactions from the database
|
| 248 |
+
def get_recent_transactions(limit=10):
|
| 249 |
+
conn = sqlite3.connect(DB_PATH)
|
| 250 |
+
cursor = conn.cursor()
|
| 251 |
+
cursor.execute("""
|
| 252 |
+
SELECT t.id, t.symbol, t.action, t.quantity, t.price, t.status,
|
| 253 |
+
t.allocation_percentage, t.timestamp, s.name as strategy_name
|
| 254 |
+
FROM transactions t
|
| 255 |
+
LEFT JOIN strategies s ON t.strategy_id = s.id
|
| 256 |
+
ORDER BY t.timestamp DESC
|
| 257 |
+
LIMIT ?
|
| 258 |
+
""", (limit,))
|
| 259 |
+
|
| 260 |
+
transactions = []
|
| 261 |
+
for row in cursor.fetchall():
|
| 262 |
+
transactions.append({
|
| 263 |
+
"id": row[0],
|
| 264 |
+
"symbol": row[1],
|
| 265 |
+
"action": row[2],
|
| 266 |
+
"quantity": row[3],
|
| 267 |
+
"price": row[4],
|
| 268 |
+
"status": row[5],
|
| 269 |
+
"allocation_percentage": row[6],
|
| 270 |
+
"timestamp": row[7],
|
| 271 |
+
"strategy_name": row[8] or "Default Strategy"
|
| 272 |
+
})
|
| 273 |
+
|
| 274 |
+
conn.close()
|
| 275 |
+
return transactions
|
| 276 |
+
|
| 277 |
+
# Function to fetch account information
|
| 278 |
+
def fetch_account_info():
|
| 279 |
+
try:
|
| 280 |
+
order_tool = AlpacaCryptoOrderTool()
|
| 281 |
+
account_data = order_tool._check_account()
|
| 282 |
+
return account_data
|
| 283 |
+
except Exception as e:
|
| 284 |
+
print(f"Error fetching account info: {e}")
|
| 285 |
+
return {"error": str(e), "cash": 0, "equity": 0}
|
| 286 |
+
|
| 287 |
+
# Function to reset portfolio (close all positions)
|
| 288 |
+
def reset_portfolio():
|
| 289 |
+
try:
|
| 290 |
+
# Create a session with authentication
|
| 291 |
+
api_key = os.getenv("ALPACA_API_KEY")
|
| 292 |
+
api_secret = os.getenv("ALPACA_API_SECRET")
|
| 293 |
+
|
| 294 |
+
headers = {
|
| 295 |
+
"APCA-API-KEY-ID": api_key,
|
| 296 |
+
"APCA-API-SECRET-KEY": api_secret
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
# Use paper trading URL
|
| 300 |
+
base_url = "https://paper-api.alpaca.markets"
|
| 301 |
+
|
| 302 |
+
# 1. First cancel all open orders
|
| 303 |
+
response = requests.delete(f"{base_url}/v2/orders", headers=headers)
|
| 304 |
+
if response.status_code != 204 and response.status_code != 200:
|
| 305 |
+
print(f"Error canceling orders: {response.status_code}, {response.text}")
|
| 306 |
+
return f"Error canceling orders: {response.status_code}"
|
| 307 |
+
|
| 308 |
+
# 2. Then close all positions
|
| 309 |
+
response = requests.delete(f"{base_url}/v2/positions", headers=headers)
|
| 310 |
+
if response.status_code != 204 and response.status_code != 200:
|
| 311 |
+
print(f"Error closing positions: {response.status_code}, {response.text}")
|
| 312 |
+
return f"Error closing positions: {response.status_code}"
|
| 313 |
+
|
| 314 |
+
return "Portfolio reset successfully. All positions closed and orders canceled."
|
| 315 |
+
except Exception as e:
|
| 316 |
+
print(f"Error resetting portfolio: {e}")
|
| 317 |
+
return f"Error resetting portfolio: {str(e)}"
|
| 318 |
+
|
| 319 |
+
# Function to fetch order history
|
| 320 |
+
def fetch_order_history():
|
| 321 |
+
try:
|
| 322 |
+
# Create a session with authentication
|
| 323 |
+
api_key = os.getenv("ALPACA_API_KEY")
|
| 324 |
+
api_secret = os.getenv("ALPACA_API_SECRET")
|
| 325 |
+
|
| 326 |
+
headers = {
|
| 327 |
+
"APCA-API-KEY-ID": api_key,
|
| 328 |
+
"APCA-API-SECRET-KEY": api_secret
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
# Use paper trading URL
|
| 332 |
+
base_url = "https://paper-api.alpaca.markets"
|
| 333 |
+
|
| 334 |
+
# Fetch orders
|
| 335 |
+
response = requests.get(f"{base_url}/v2/orders?status=all&limit=100", headers=headers)
|
| 336 |
+
|
| 337 |
+
if response.status_code == 200:
|
| 338 |
+
orders = response.json()
|
| 339 |
+
# Filter for BTC orders and format them
|
| 340 |
+
btc_orders = [order for order in orders if "BTC" in order.get("symbol", "")]
|
| 341 |
+
|
| 342 |
+
formatted_orders = []
|
| 343 |
+
for order in btc_orders:
|
| 344 |
+
formatted_orders.append({
|
| 345 |
+
"id": order["id"],
|
| 346 |
+
"symbol": order["symbol"],
|
| 347 |
+
"side": order["side"],
|
| 348 |
+
"type": order["type"],
|
| 349 |
+
"qty": order["qty"],
|
| 350 |
+
"status": order["status"],
|
| 351 |
+
"created_at": order["created_at"],
|
| 352 |
+
"filled_at": order.get("filled_at", "N/A"),
|
| 353 |
+
"filled_qty": order.get("filled_qty", "0"),
|
| 354 |
+
"filled_avg_price": order.get("filled_avg_price", "0")
|
| 355 |
+
})
|
| 356 |
+
|
| 357 |
+
return formatted_orders
|
| 358 |
+
else:
|
| 359 |
+
print(f"Error fetching orders: {response.status_code}, {response.text}")
|
| 360 |
+
return []
|
| 361 |
+
except Exception as e:
|
| 362 |
+
print(f"Error in fetch_order_history: {e}")
|
| 363 |
+
return []
|
| 364 |
+
|
| 365 |
+
# Function to fetch active positions
|
| 366 |
+
def fetch_active_positions():
|
| 367 |
+
try:
|
| 368 |
+
api_key = os.getenv("ALPACA_API_KEY")
|
| 369 |
+
api_secret = os.getenv("ALPACA_API_SECRET")
|
| 370 |
+
|
| 371 |
+
headers = {
|
| 372 |
+
"APCA-API-KEY-ID": api_key,
|
| 373 |
+
"APCA-API-SECRET-KEY": api_secret
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
base_url = "https://paper-api.alpaca.markets"
|
| 377 |
+
|
| 378 |
+
# Fetch positions
|
| 379 |
+
response = requests.get(f"{base_url}/v2/positions", headers=headers)
|
| 380 |
+
|
| 381 |
+
if response.status_code == 200:
|
| 382 |
+
positions = response.json()
|
| 383 |
+
# Filter for BTC positions
|
| 384 |
+
btc_positions = [pos for pos in positions if "BTC" in pos.get("symbol", "")]
|
| 385 |
+
|
| 386 |
+
formatted_positions = []
|
| 387 |
+
for pos in btc_positions:
|
| 388 |
+
# Calculate profit/loss
|
| 389 |
+
current_price = float(pos.get("current_price", 0))
|
| 390 |
+
avg_entry_price = float(pos.get("avg_entry_price", 0))
|
| 391 |
+
qty = float(pos.get("qty", 0))
|
| 392 |
+
|
| 393 |
+
profit_loss = (current_price - avg_entry_price) * qty
|
| 394 |
+
profit_loss_percent = ((current_price / avg_entry_price) - 1) * 100 if avg_entry_price > 0 else 0
|
| 395 |
+
|
| 396 |
+
formatted_positions.append({
|
| 397 |
+
"symbol": pos["symbol"],
|
| 398 |
+
"qty": pos["qty"],
|
| 399 |
+
"avg_entry_price": pos["avg_entry_price"],
|
| 400 |
+
"current_price": pos["current_price"],
|
| 401 |
+
"profit_loss": round(profit_loss, 2),
|
| 402 |
+
"profit_loss_percent": round(profit_loss_percent, 2),
|
| 403 |
+
"market_value": pos["market_value"],
|
| 404 |
+
"side": pos["side"]
|
| 405 |
+
})
|
| 406 |
+
|
| 407 |
+
return formatted_positions
|
| 408 |
+
else:
|
| 409 |
+
print(f"Error fetching positions: {response.status_code}, {response.text}")
|
| 410 |
+
return []
|
| 411 |
+
except Exception as e:
|
| 412 |
+
print(f"Error in fetch_active_positions: {e}")
|
| 413 |
+
return []
|
| 414 |
+
|
| 415 |
+
# Function to run technical analysis with just the TA agent
|
| 416 |
+
def run_ta_agent_only(strategy_text=None):
|
| 417 |
+
try:
|
| 418 |
+
tech_strategy = TechnicalAnalysisStrategy()
|
| 419 |
+
|
| 420 |
+
# Use the provided strategy text or the global one
|
| 421 |
+
strategy_text = strategy_text or strategy_params.get("strategy_text")
|
| 422 |
+
|
| 423 |
+
if not strategy_text:
|
| 424 |
+
return {
|
| 425 |
+
"error": "No strategy text provided",
|
| 426 |
+
"signal": "hold",
|
| 427 |
+
"confidence": 0,
|
| 428 |
+
"allocation_percentage": 0,
|
| 429 |
+
"reasoning": "Please enter a strategy description first"
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
# Get the indicator data from the tool
|
| 433 |
+
indicator_data = tech_strategy._run()
|
| 434 |
+
|
| 435 |
+
if "error" in indicator_data:
|
| 436 |
+
return {
|
| 437 |
+
"error": indicator_data["error"],
|
| 438 |
+
"signal": "hold",
|
| 439 |
+
"confidence": 0,
|
| 440 |
+
"allocation_percentage": 0,
|
| 441 |
+
"reasoning": f"Error fetching indicator data: {indicator_data['error']}"
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
# Use OpenAI to interpret the strategy based on the indicator data
|
| 445 |
+
signal_data = interpret_strategy_with_llm(indicator_data, strategy_text, strategy_params["max_allocation_percentage"])
|
| 446 |
+
|
| 447 |
+
# Add timestamp
|
| 448 |
+
signal_data["timestamp"] = datetime.now().isoformat()
|
| 449 |
+
|
| 450 |
+
# Add to analysis results
|
| 451 |
+
analysis_results.append(signal_data)
|
| 452 |
+
|
| 453 |
+
# Save to database
|
| 454 |
+
save_analysis_result(signal_data)
|
| 455 |
+
|
| 456 |
+
return signal_data
|
| 457 |
+
except Exception as e:
|
| 458 |
+
print(f"Error running TA agent: {e}")
|
| 459 |
+
return {"error": str(e)}
|
| 460 |
+
|
| 461 |
+
# New function to interpret strategy with LLM
|
| 462 |
+
def interpret_strategy_with_llm(indicator_data, strategy_text, max_allocation_percentage=50):
|
| 463 |
+
try:
|
| 464 |
+
# Create the system prompt for the LLM
|
| 465 |
+
system_prompt = """
|
| 466 |
+
You are a cryptocurrency trading strategy interpreter. Your task is to analyze the provided technical
|
| 467 |
+
indicators and price data, then interpret the user's strategy to generate a trading signal.
|
| 468 |
+
|
| 469 |
+
You must return a JSON object with the following fields:
|
| 470 |
+
- signal: "buy", "sell", or "hold"
|
| 471 |
+
- confidence: Integer between 0-95 (how confident you are in the signal)
|
| 472 |
+
- allocation_percentage: Integer between 0-{max_allocation} (how much of the portfolio to allocate)
|
| 473 |
+
- reasoning: String explanation of your decision process
|
| 474 |
+
|
| 475 |
+
Be pragmatic and conservative. Only give buy/sell signals when the conditions are clearly met.
|
| 476 |
+
Base your decision on the indicator values, not on general market sentiment or news.
|
| 477 |
+
"""
|
| 478 |
+
|
| 479 |
+
# Prepare price data - might need to be retrieved from indicator_data
|
| 480 |
+
price = indicator_data.get("price", 0)
|
| 481 |
+
|
| 482 |
+
# Create the user prompt with all the data
|
| 483 |
+
user_prompt = f"""
|
| 484 |
+
# Technical Indicators
|
| 485 |
+
{json.dumps(indicator_data, indent=2)}
|
| 486 |
+
|
| 487 |
+
# Strategy Description
|
| 488 |
+
{strategy_text}
|
| 489 |
+
|
| 490 |
+
Analyze the above data according to the strategy description and generate a trading signal.
|
| 491 |
+
Respond only with JSON. Maximum allocation is {max_allocation_percentage}%.
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
# Make the call to OpenAI
|
| 495 |
+
response = openai.chat.completions.create(
|
| 496 |
+
model="gpt-3.5-turbo",
|
| 497 |
+
messages=[
|
| 498 |
+
{"role": "system", "content": system_prompt.format(max_allocation=max_allocation_percentage)},
|
| 499 |
+
{"role": "user", "content": user_prompt}
|
| 500 |
+
],
|
| 501 |
+
temperature=0.2,
|
| 502 |
+
response_format={"type": "json_object"}
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
# Extract and parse the response content
|
| 506 |
+
response_content = response.choices[0].message.content
|
| 507 |
+
result = json.loads(response_content)
|
| 508 |
+
|
| 509 |
+
# Ensure the result has the expected fields
|
| 510 |
+
if not all(k in result for k in ["signal", "confidence", "allocation_percentage", "reasoning"]):
|
| 511 |
+
missing = [k for k in ["signal", "confidence", "allocation_percentage", "reasoning"] if k not in result]
|
| 512 |
+
print(f"LLM response missing required fields: {missing}")
|
| 513 |
+
result = {
|
| 514 |
+
"signal": result.get("signal", "hold"),
|
| 515 |
+
"confidence": result.get("confidence", 50),
|
| 516 |
+
"allocation_percentage": result.get("allocation_percentage", 0),
|
| 517 |
+
"reasoning": result.get("reasoning", "No reasoning provided.")
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
# Ensure values are within expected ranges
|
| 521 |
+
result["confidence"] = max(0, min(95, int(result["confidence"])))
|
| 522 |
+
result["allocation_percentage"] = max(0, min(max_allocation_percentage, int(result["allocation_percentage"])))
|
| 523 |
+
|
| 524 |
+
# Add indicator values to the result
|
| 525 |
+
result["technical_indicators"] = indicator_data
|
| 526 |
+
|
| 527 |
+
print(f"LLM generated signal: {result['signal']} with confidence {result['confidence']}%")
|
| 528 |
+
return result
|
| 529 |
+
|
| 530 |
+
except Exception as e:
|
| 531 |
+
print(f"Error interpreting strategy with LLM: {e}")
|
| 532 |
+
import traceback
|
| 533 |
+
traceback.print_exc()
|
| 534 |
+
return {
|
| 535 |
+
"signal": "hold",
|
| 536 |
+
"confidence": 0,
|
| 537 |
+
"allocation_percentage": 0,
|
| 538 |
+
"reasoning": f"Error interpreting strategy with LLM: {str(e)}"
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
# Function to run the full analysis using the crew
|
| 542 |
+
def run_full_analysis():
|
| 543 |
+
try:
|
| 544 |
+
# Get the strategy text from global parameters
|
| 545 |
+
strategy_text = strategy_params.get("strategy_text")
|
| 546 |
+
|
| 547 |
+
if not strategy_text:
|
| 548 |
+
return {
|
| 549 |
+
"error": "No strategy text provided",
|
| 550 |
+
"signal": "hold",
|
| 551 |
+
"confidence": 0,
|
| 552 |
+
"allocation_percentage": 0,
|
| 553 |
+
"reasoning": "Please enter a strategy description first"
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
crew = BitcoinAnalysisCrew()
|
| 557 |
+
result = crew.run_analysis(strategy_text=strategy_text)
|
| 558 |
+
|
| 559 |
+
# Add timestamp
|
| 560 |
+
result["timestamp"] = datetime.now().isoformat()
|
| 561 |
+
|
| 562 |
+
# Add to analysis results
|
| 563 |
+
analysis_results.append(result)
|
| 564 |
+
|
| 565 |
+
# Save to database
|
| 566 |
+
save_analysis_result(result)
|
| 567 |
+
|
| 568 |
+
# Check if order was executed, if so add to orders
|
| 569 |
+
if "order_execution" in result and isinstance(result["order_execution"], dict):
|
| 570 |
+
if result["order_execution"].get("success", False):
|
| 571 |
+
orders_history.append(result["order_execution"])
|
| 572 |
+
save_transaction(result["order_execution"])
|
| 573 |
+
|
| 574 |
+
return result
|
| 575 |
+
except Exception as e:
|
| 576 |
+
print(f"Error running crew analysis: {e}")
|
| 577 |
+
return {"error": str(e)}
|
| 578 |
+
|
| 579 |
+
# Background process function
|
| 580 |
+
def background_process():
|
| 581 |
+
global running
|
| 582 |
+
|
| 583 |
+
# Create a log file for the automated trading session
|
| 584 |
+
log_file = os.path.join(os.path.dirname(DB_PATH), f"auto_trading_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
|
| 585 |
+
with open(log_file, 'w') as f:
|
| 586 |
+
f.write(f"Automated trading session started at {datetime.now().isoformat()}\n")
|
| 587 |
+
f.write(f"Strategy parameters: {json.dumps(strategy_params)}\n\n")
|
| 588 |
+
|
| 589 |
+
error_count = 0
|
| 590 |
+
max_errors = 5
|
| 591 |
+
|
| 592 |
+
while running:
|
| 593 |
+
try:
|
| 594 |
+
print(f"Running full crew analysis at {datetime.now().isoformat()} with params: {strategy_params}")
|
| 595 |
+
|
| 596 |
+
# Log to file as well
|
| 597 |
+
with open(log_file, 'a') as f:
|
| 598 |
+
f.write(f"\n--- Analysis run at {datetime.now().isoformat()} ---\n")
|
| 599 |
+
|
| 600 |
+
# Check if strategy text is available
|
| 601 |
+
if not strategy_params.get("strategy_text"):
|
| 602 |
+
print("No strategy text available for automated analysis. Skipping.")
|
| 603 |
+
with open(log_file, 'a') as f:
|
| 604 |
+
f.write("No strategy text available for automated analysis. Skipping.\n")
|
| 605 |
+
time.sleep(60) # Sleep for a minute and check again
|
| 606 |
+
continue
|
| 607 |
+
|
| 608 |
+
# Run the full crew analysis instead of just TA agent
|
| 609 |
+
result = run_full_analysis()
|
| 610 |
+
print(f"Analysis result: {result.get('signal', 'unknown')} with confidence {result.get('confidence', 0)}%")
|
| 611 |
+
|
| 612 |
+
# Log the result
|
| 613 |
+
with open(log_file, 'a') as f:
|
| 614 |
+
f.write(f"Signal: {result.get('signal', 'unknown')}, Confidence: {result.get('confidence', 0)}%, Allocation: {result.get('allocation_percentage', 0)}%\n")
|
| 615 |
+
f.write(f"Reasoning: {result.get('reasoning', 'No reasoning provided')}\n")
|
| 616 |
+
|
| 617 |
+
# Log order execution details if any
|
| 618 |
+
if "order_execution" in result and result["order_execution"]:
|
| 619 |
+
order_details = result["order_execution"]
|
| 620 |
+
if isinstance(order_details, dict):
|
| 621 |
+
f.write(f"Order executed: {order_details.get('action', 'unknown')} {order_details.get('quantity', 0)} BTC at ${order_details.get('price', 0)}\n")
|
| 622 |
+
else:
|
| 623 |
+
f.write(f"Order info: {str(order_details)}\n")
|
| 624 |
+
|
| 625 |
+
# Update the active trades
|
| 626 |
+
positions = fetch_active_positions()
|
| 627 |
+
global active_trades
|
| 628 |
+
active_trades = positions
|
| 629 |
+
|
| 630 |
+
# Log current positions
|
| 631 |
+
with open(log_file, 'a') as f:
|
| 632 |
+
f.write("\nCurrent Positions:\n")
|
| 633 |
+
if positions:
|
| 634 |
+
for pos in positions:
|
| 635 |
+
f.write(f" {pos['symbol']}: {pos['qty']} @ ${pos['avg_entry_price']} - P/L: ${pos['profit_loss']} ({pos['profit_loss_percent']}%)\n")
|
| 636 |
+
else:
|
| 637 |
+
f.write(" No active positions\n")
|
| 638 |
+
|
| 639 |
+
# Log account info
|
| 640 |
+
account = fetch_account_info()
|
| 641 |
+
f.write(f"\nAccount Balance: ${account.get('cash', 'N/A')}, Equity: ${account.get('equity', 'N/A')}\n")
|
| 642 |
+
|
| 643 |
+
# Reset error counter on successful run
|
| 644 |
+
error_count = 0
|
| 645 |
+
|
| 646 |
+
# Sleep for the specified timeframe interval
|
| 647 |
+
interval_seconds = strategy_params["timeframe_minutes"] * 60
|
| 648 |
+
print(f"Sleeping for {interval_seconds} seconds ({strategy_params['timeframe_minutes']} minutes)")
|
| 649 |
+
|
| 650 |
+
# Sleep in smaller increments to allow for quicker stopping
|
| 651 |
+
for _ in range(min(interval_seconds, 3600), 0, -10): # Sleep in 10-second increments
|
| 652 |
+
if not running:
|
| 653 |
+
break
|
| 654 |
+
time.sleep(10)
|
| 655 |
+
|
| 656 |
+
except Exception as e:
|
| 657 |
+
error_count += 1
|
| 658 |
+
error_message = f"Error in background process: {str(e)}"
|
| 659 |
+
print(error_message)
|
| 660 |
+
import traceback
|
| 661 |
+
trace = traceback.format_exc()
|
| 662 |
+
print(trace)
|
| 663 |
+
|
| 664 |
+
# Log error
|
| 665 |
+
with open(log_file, 'a') as f:
|
| 666 |
+
f.write(f"\nERROR: {error_message}\n")
|
| 667 |
+
f.write(trace + "\n")
|
| 668 |
+
|
| 669 |
+
# If too many consecutive errors, pause for a longer time
|
| 670 |
+
if error_count >= max_errors:
|
| 671 |
+
print(f"Too many errors ({error_count}). Pausing for 30 minutes.")
|
| 672 |
+
with open(log_file, 'a') as f:
|
| 673 |
+
f.write(f"Too many errors ({error_count}). Pausing for 30 minutes.\n")
|
| 674 |
+
|
| 675 |
+
# Sleep but still check for stop signal
|
| 676 |
+
for _ in range(1800, 0, -10):
|
| 677 |
+
if not running:
|
| 678 |
+
break
|
| 679 |
+
time.sleep(10)
|
| 680 |
+
|
| 681 |
+
# Reset error count after pause
|
| 682 |
+
error_count = 0
|
| 683 |
+
else:
|
| 684 |
+
# Short pause before retrying
|
| 685 |
+
time.sleep(60)
|
| 686 |
+
|
| 687 |
+
# Function to start the background process
|
| 688 |
+
def start_background_process():
|
| 689 |
+
global running, background_thread
|
| 690 |
+
|
| 691 |
+
if not running:
|
| 692 |
+
running = True
|
| 693 |
+
background_thread = threading.Thread(target=background_process)
|
| 694 |
+
background_thread.daemon = True
|
| 695 |
+
background_thread.start()
|
| 696 |
+
return f"Background analysis started. Running full crew analysis every {strategy_params['timeframe_minutes']} minutes."
|
| 697 |
+
else:
|
| 698 |
+
return "Background analysis is already running."
|
| 699 |
+
|
| 700 |
+
# Function to stop the background process
|
| 701 |
+
def stop_background_process():
|
| 702 |
+
global running
|
| 703 |
+
|
| 704 |
+
if running:
|
| 705 |
+
running = False
|
| 706 |
+
return "Background analysis stopped."
|
| 707 |
+
else:
|
| 708 |
+
return "Background analysis is not running."
|
| 709 |
+
|
| 710 |
+
# Function to update strategy parameters
|
| 711 |
+
def update_strategy(timeframe, max_allocation):
|
| 712 |
+
global strategy_params
|
| 713 |
+
|
| 714 |
+
try:
|
| 715 |
+
# Convert values to appropriate types
|
| 716 |
+
timeframe = int(timeframe)
|
| 717 |
+
max_allocation = int(max_allocation)
|
| 718 |
+
|
| 719 |
+
# Update the global dictionary
|
| 720 |
+
strategy_params["timeframe_minutes"] = timeframe
|
| 721 |
+
strategy_params["max_allocation_percentage"] = max_allocation
|
| 722 |
+
|
| 723 |
+
print(f"Updated strategy parameters: {strategy_params}")
|
| 724 |
+
|
| 725 |
+
return f"Strategy parameters updated: Timeframe: {timeframe} minutes, Max allocation: {max_allocation}%"
|
| 726 |
+
except Exception as e:
|
| 727 |
+
return f"Error updating strategy parameters: {e}"
|
| 728 |
+
|
| 729 |
+
# Function to update custom strategy text
|
| 730 |
+
def update_strategy_text(strategy_text):
|
| 731 |
+
global strategy_params
|
| 732 |
+
|
| 733 |
+
try:
|
| 734 |
+
strategy_params["strategy_text"] = strategy_text
|
| 735 |
+
print(f"Updated strategy text: {strategy_text[:100]}...")
|
| 736 |
+
return "Strategy text updated successfully"
|
| 737 |
+
except Exception as e:
|
| 738 |
+
return f"Error updating strategy text: {e}"
|
| 739 |
+
|
| 740 |
+
# Function to save current strategy
|
| 741 |
+
def save_current_strategy(name, description):
|
| 742 |
+
try:
|
| 743 |
+
# Get current strategy parameters
|
| 744 |
+
parameters = strategy_params.copy()
|
| 745 |
+
strategy_text = parameters.get("strategy_text", "")
|
| 746 |
+
|
| 747 |
+
# If strategy text is empty, provide a default description
|
| 748 |
+
if not strategy_text:
|
| 749 |
+
strategy_text = f"Default RSI ({parameters['rsi_lower_threshold']}-{parameters['rsi_upper_threshold']}) and Bollinger Bands strategy"
|
| 750 |
+
|
| 751 |
+
# Save to database
|
| 752 |
+
success, message = save_strategy(name, description, strategy_text, parameters)
|
| 753 |
+
|
| 754 |
+
if success:
|
| 755 |
+
return f"Strategy '{name}' saved successfully"
|
| 756 |
+
else:
|
| 757 |
+
return message
|
| 758 |
+
except Exception as e:
|
| 759 |
+
return f"Error saving strategy: {e}"
|
| 760 |
+
|
| 761 |
+
# Function to run a manual trade
|
| 762 |
+
def execute_trade(action, symbol, allocation_pct):
|
| 763 |
+
try:
|
| 764 |
+
order_tool = AlpacaCryptoOrderTool()
|
| 765 |
+
|
| 766 |
+
result = order_tool._run(
|
| 767 |
+
action=action,
|
| 768 |
+
symbol=symbol,
|
| 769 |
+
allocation_percentage=int(allocation_pct)
|
| 770 |
+
)
|
| 771 |
+
|
| 772 |
+
if result.get("success", False):
|
| 773 |
+
orders_history.append(result)
|
| 774 |
+
# Save to database
|
| 775 |
+
save_transaction(result)
|
| 776 |
+
return f"Trade executed: {action.upper()} {symbol} with {allocation_pct}% allocation"
|
| 777 |
+
else:
|
| 778 |
+
return f"Trade failed: {result.get('error', 'Unknown error')}"
|
| 779 |
+
except Exception as e:
|
| 780 |
+
return f"Error executing trade: {e}"
|
| 781 |
+
|
| 782 |
+
# Function to get account summary for display
|
| 783 |
+
def get_account_summary():
|
| 784 |
+
account = fetch_account_info()
|
| 785 |
+
positions = fetch_active_positions()
|
| 786 |
+
|
| 787 |
+
cash = account.get("cash", "0")
|
| 788 |
+
equity = account.get("equity", "0")
|
| 789 |
+
|
| 790 |
+
total_positions = len(positions)
|
| 791 |
+
total_value = sum(float(pos.get("market_value", 0)) for pos in positions)
|
| 792 |
+
total_pl = sum(pos.get("profit_loss", 0) for pos in positions)
|
| 793 |
+
|
| 794 |
+
return f"""
|
| 795 |
+
**Account Summary**
|
| 796 |
+
- Cash: ${cash}
|
| 797 |
+
- Equity: ${equity}
|
| 798 |
+
- Active Positions: {total_positions}
|
| 799 |
+
- Positions Value: ${total_value:.2f}
|
| 800 |
+
- Total P/L: ${total_pl:.2f}
|
| 801 |
+
"""
|
| 802 |
+
|
| 803 |
+
# Function to format analysis results for display
|
| 804 |
+
def format_analysis_results():
|
| 805 |
+
# First try to get from database
|
| 806 |
+
db_results = get_recent_analysis_results(1)
|
| 807 |
+
|
| 808 |
+
if db_results:
|
| 809 |
+
latest = db_results[0]
|
| 810 |
+
|
| 811 |
+
timestamp = latest.get("timestamp", datetime.now().isoformat())
|
| 812 |
+
signal = latest.get("signal", "unknown").upper()
|
| 813 |
+
confidence = latest.get("confidence", 0)
|
| 814 |
+
allocation = latest.get("allocation_percentage", 0)
|
| 815 |
+
reasoning = latest.get("reasoning", "No reasoning provided.")
|
| 816 |
+
strategy_name = latest.get("strategy_name", "Default Strategy")
|
| 817 |
+
|
| 818 |
+
return f"""
|
| 819 |
+
**Latest Analysis ({timestamp})**
|
| 820 |
+
|
| 821 |
+
Strategy: {strategy_name}
|
| 822 |
+
Signal: {signal}
|
| 823 |
+
Confidence: {confidence}%
|
| 824 |
+
Allocation: {allocation}%
|
| 825 |
+
|
| 826 |
+
Reasoning:
|
| 827 |
+
{reasoning}
|
| 828 |
+
"""
|
| 829 |
+
|
| 830 |
+
# Fallback to memory if database is empty
|
| 831 |
+
if not analysis_results:
|
| 832 |
+
return "No analysis results available."
|
| 833 |
+
|
| 834 |
+
latest = analysis_results[-1]
|
| 835 |
+
|
| 836 |
+
timestamp = latest.get("timestamp", datetime.now().isoformat())
|
| 837 |
+
signal = latest.get("signal", "unknown").upper()
|
| 838 |
+
confidence = latest.get("confidence", 0)
|
| 839 |
+
allocation = latest.get("allocation_percentage", 0)
|
| 840 |
+
reasoning = latest.get("reasoning", "No reasoning provided.")
|
| 841 |
+
|
| 842 |
+
return f"""
|
| 843 |
+
**Latest Analysis ({timestamp})**
|
| 844 |
+
|
| 845 |
+
Signal: {signal}
|
| 846 |
+
Confidence: {confidence}%
|
| 847 |
+
Allocation: {allocation}%
|
| 848 |
+
|
| 849 |
+
Reasoning:
|
| 850 |
+
{reasoning}
|
| 851 |
+
"""
|
| 852 |
+
|
| 853 |
+
# Function to format active positions for display
|
| 854 |
+
def format_active_positions():
|
| 855 |
+
positions = fetch_active_positions()
|
| 856 |
+
|
| 857 |
+
if not positions:
|
| 858 |
+
return "No active positions."
|
| 859 |
+
|
| 860 |
+
result = "## Active Positions\n\n"
|
| 861 |
+
|
| 862 |
+
for pos in positions:
|
| 863 |
+
result += f"""
|
| 864 |
+
**{pos['symbol']}**
|
| 865 |
+
Quantity: {pos['qty']} BTC
|
| 866 |
+
Entry: ${pos['avg_entry_price']}
|
| 867 |
+
Current: ${pos['current_price']}
|
| 868 |
+
P/L: ${pos['profit_loss']} ({pos['profit_loss_percent']}%)
|
| 869 |
+
Value: ${pos['market_value']}
|
| 870 |
+
|
| 871 |
+
"""
|
| 872 |
+
|
| 873 |
+
return result
|
| 874 |
+
|
| 875 |
+
# Function to format order history for display
|
| 876 |
+
def format_order_history():
|
| 877 |
+
# Try to get from database first
|
| 878 |
+
db_transactions = get_recent_transactions(10)
|
| 879 |
+
|
| 880 |
+
if db_transactions:
|
| 881 |
+
result = "## Recent Transactions\n\n"
|
| 882 |
+
|
| 883 |
+
for tx in db_transactions:
|
| 884 |
+
result += f"""
|
| 885 |
+
**{tx['symbol']} {tx['action'].upper()}**
|
| 886 |
+
Quantity: {tx['quantity']}
|
| 887 |
+
Price: ${tx['price']}
|
| 888 |
+
Status: {tx['status']}
|
| 889 |
+
Allocation: {tx['allocation_percentage']}%
|
| 890 |
+
Date: {tx['timestamp']}
|
| 891 |
+
Strategy: {tx['strategy_name']}
|
| 892 |
+
|
| 893 |
+
"""
|
| 894 |
+
|
| 895 |
+
return result
|
| 896 |
+
|
| 897 |
+
# Fall back to API if database is empty
|
| 898 |
+
orders = fetch_order_history()
|
| 899 |
+
|
| 900 |
+
if not orders:
|
| 901 |
+
return "No order history."
|
| 902 |
+
|
| 903 |
+
result = "## Order History (Last 10)\n\n"
|
| 904 |
+
|
| 905 |
+
for order in orders[:10]:
|
| 906 |
+
result += f"""
|
| 907 |
+
**{order['symbol']} {order['side'].upper()}**
|
| 908 |
+
Quantity: {order['qty']}
|
| 909 |
+
Type: {order['type']}
|
| 910 |
+
Status: {order['status']}
|
| 911 |
+
Created: {order['created_at']}
|
| 912 |
+
Filled: {order.get('filled_at', 'N/A')}
|
| 913 |
+
Filled Price: ${order.get('filled_avg_price', 'N/A')}
|
| 914 |
+
|
| 915 |
+
"""
|
| 916 |
+
|
| 917 |
+
return result
|
| 918 |
+
|
| 919 |
+
# Function to get available indicators for display
|
| 920 |
+
def get_available_indicators():
|
| 921 |
+
indicators = IndicatorCalculator.get_available_indicators()
|
| 922 |
+
|
| 923 |
+
result = "## Available Indicators\n\n"
|
| 924 |
+
|
| 925 |
+
for name, description in indicators.items():
|
| 926 |
+
result += f"**{name}**: {description}\n\n"
|
| 927 |
+
|
| 928 |
+
return result
|
| 929 |
+
|
| 930 |
+
# Function to format detailed analysis results for display
|
| 931 |
+
def format_detailed_analysis_results(result=None):
|
| 932 |
+
if result is None:
|
| 933 |
+
# First try to get from database
|
| 934 |
+
db_results = get_recent_analysis_results(1)
|
| 935 |
+
if db_results:
|
| 936 |
+
result = db_results[0]
|
| 937 |
+
elif analysis_results:
|
| 938 |
+
result = analysis_results[-1]
|
| 939 |
+
else:
|
| 940 |
+
return "No analysis results available."
|
| 941 |
+
|
| 942 |
+
# Basic information
|
| 943 |
+
timestamp = result.get("timestamp", datetime.now().isoformat())
|
| 944 |
+
signal = result.get("signal", "unknown").upper()
|
| 945 |
+
confidence = result.get("confidence", 0)
|
| 946 |
+
allocation = result.get("allocation_percentage", 0)
|
| 947 |
+
reasoning = result.get("reasoning", "No reasoning provided.")
|
| 948 |
+
strategy_name = result.get("strategy_name", "Default Strategy")
|
| 949 |
+
|
| 950 |
+
# Detailed sections
|
| 951 |
+
sections = []
|
| 952 |
+
|
| 953 |
+
# Add header
|
| 954 |
+
sections.append(f"## Analysis Results ({timestamp})")
|
| 955 |
+
sections.append(f"### Strategy: {strategy_name}")
|
| 956 |
+
sections.append(f"### Signal: {signal} | Confidence: {confidence}% | Allocation: {allocation}%")
|
| 957 |
+
|
| 958 |
+
# Technical indicator values (if available)
|
| 959 |
+
if "technical_indicators" in result:
|
| 960 |
+
sections.append("### Technical Indicators")
|
| 961 |
+
indicators = result["technical_indicators"]
|
| 962 |
+
indicators_text = []
|
| 963 |
+
|
| 964 |
+
# Display price first
|
| 965 |
+
if "price" in indicators:
|
| 966 |
+
indicators_text.append(f"- **Price**: ${indicators['price']:.2f}")
|
| 967 |
+
|
| 968 |
+
# Display RSI values
|
| 969 |
+
rsi_indicators = {k: v for k, v in indicators.items() if "rsi" in k.lower() and v is not None}
|
| 970 |
+
if rsi_indicators:
|
| 971 |
+
indicators_text.append("- **RSI**:")
|
| 972 |
+
for k, v in rsi_indicators.items():
|
| 973 |
+
indicators_text.append(f" - {k}: {v:.2f}")
|
| 974 |
+
|
| 975 |
+
# Display Bollinger Bands
|
| 976 |
+
bb_indicators = {k: v for k, v in indicators.items() if "bb_" in k.lower() and v is not None}
|
| 977 |
+
if bb_indicators:
|
| 978 |
+
indicators_text.append("- **Bollinger Bands**:")
|
| 979 |
+
for k, v in bb_indicators.items():
|
| 980 |
+
indicators_text.append(f" - {k}: {v:.2f}")
|
| 981 |
+
|
| 982 |
+
# Display MACD
|
| 983 |
+
macd_indicators = {k: v for k, v in indicators.items() if "macd" in k.lower() and v is not None}
|
| 984 |
+
if macd_indicators:
|
| 985 |
+
indicators_text.append("- **MACD**:")
|
| 986 |
+
for k, v in macd_indicators.items():
|
| 987 |
+
indicators_text.append(f" - {k}: {v:.2f}")
|
| 988 |
+
|
| 989 |
+
# Display other indicators
|
| 990 |
+
other_indicators = {k: v for k, v in indicators.items()
|
| 991 |
+
if not any(x in k.lower() for x in ["rsi", "bb_", "macd", "price"])
|
| 992 |
+
and v is not None}
|
| 993 |
+
if other_indicators:
|
| 994 |
+
indicators_text.append("- **Other Indicators**:")
|
| 995 |
+
for k, v in other_indicators.items():
|
| 996 |
+
indicators_text.append(f" - {k}: {v:.2f}")
|
| 997 |
+
|
| 998 |
+
sections.append("\n".join(indicators_text))
|
| 999 |
+
|
| 1000 |
+
# Add technical analysis section
|
| 1001 |
+
if "technical_analysis" in result:
|
| 1002 |
+
sections.append("### Technical Analysis")
|
| 1003 |
+
sections.append(f"```\n{result['technical_analysis']}\n```")
|
| 1004 |
+
|
| 1005 |
+
# Add initial analysis section
|
| 1006 |
+
if "initial_analysis" in result:
|
| 1007 |
+
sections.append("### Market Context Analysis")
|
| 1008 |
+
sections.append(f"```\n{result['initial_analysis']}\n```")
|
| 1009 |
+
|
| 1010 |
+
# Add reflection analysis section
|
| 1011 |
+
if "reflection_analysis" in result:
|
| 1012 |
+
sections.append("### Sentiment Analysis")
|
| 1013 |
+
sections.append(f"```\n{result['reflection_analysis']}\n```")
|
| 1014 |
+
|
| 1015 |
+
# Add tool errors section if present
|
| 1016 |
+
if "tool_error_summary" in result or "tool_error_assessment" in result:
|
| 1017 |
+
sections.append("### Data Limitations")
|
| 1018 |
+
if "tool_error_assessment" in result:
|
| 1019 |
+
sections.append(f"```\n{result['tool_error_assessment']}\n```")
|
| 1020 |
+
if "tool_error_summary" in result:
|
| 1021 |
+
sections.append(f"Tool Errors: {result['tool_error_summary']}")
|
| 1022 |
+
|
| 1023 |
+
# Add detailed reasoning
|
| 1024 |
+
sections.append("### Detailed Reasoning")
|
| 1025 |
+
sections.append(f"```\n{reasoning}\n```")
|
| 1026 |
+
|
| 1027 |
+
# Add market outlook if available
|
| 1028 |
+
if "market_outlook" in result:
|
| 1029 |
+
sections.append("### Market Outlook")
|
| 1030 |
+
sections.append(f"```\n{result['market_outlook']}\n```")
|
| 1031 |
+
|
| 1032 |
+
# Add risk assessment if available
|
| 1033 |
+
if "risk_assessment" in result:
|
| 1034 |
+
sections.append("### Risk Assessment")
|
| 1035 |
+
sections.append(f"```\n{result['risk_assessment']}\n```")
|
| 1036 |
+
|
| 1037 |
+
# Add order execution details if available
|
| 1038 |
+
if "order_execution" in result:
|
| 1039 |
+
sections.append("### Trade Execution")
|
| 1040 |
+
if isinstance(result["order_execution"], dict):
|
| 1041 |
+
order = result["order_execution"]
|
| 1042 |
+
order_details = ["- **Status**: Success"]
|
| 1043 |
+
for k, v in order.items():
|
| 1044 |
+
if k != "success":
|
| 1045 |
+
order_details.append(f"- **{k.replace('_', ' ').title()}**: {v}")
|
| 1046 |
+
sections.append("\n".join(order_details))
|
| 1047 |
+
else:
|
| 1048 |
+
sections.append(str(result["order_execution"]))
|
| 1049 |
+
|
| 1050 |
+
return "\n\n".join(sections)
|
| 1051 |
+
|
| 1052 |
+
# Create the Gradio interface
|
| 1053 |
+
with gr.Blocks(title="CrypticAI - Bitcoin Trading Dashboard") as app:
|
| 1054 |
+
gr.Markdown("# CrypticAI - Bitcoin Trading Dashboard")
|
| 1055 |
+
|
| 1056 |
+
with gr.Tabs():
|
| 1057 |
+
# Strategy Tab
|
| 1058 |
+
with gr.TabItem("Strategy Configuration"):
|
| 1059 |
+
gr.Markdown("## Strategy Parameters")
|
| 1060 |
+
|
| 1061 |
+
with gr.Row():
|
| 1062 |
+
with gr.Column():
|
| 1063 |
+
timeframe = gr.Slider(minimum=1, maximum=240, value=strategy_params["timeframe_minutes"], step=1,
|
| 1064 |
+
label="Timeframe (minutes)")
|
| 1065 |
+
|
| 1066 |
+
with gr.Column():
|
| 1067 |
+
max_allocation = gr.Slider(minimum=5, maximum=100, value=strategy_params["max_allocation_percentage"], step=5,
|
| 1068 |
+
label="Maximum Allocation (%)")
|
| 1069 |
+
|
| 1070 |
+
strategy_update_btn = gr.Button("Update Strategy Parameters")
|
| 1071 |
+
strategy_message = gr.Textbox(label="Strategy Update Status")
|
| 1072 |
+
|
| 1073 |
+
strategy_update_btn.click(update_strategy,
|
| 1074 |
+
inputs=[timeframe, max_allocation],
|
| 1075 |
+
outputs=strategy_message)
|
| 1076 |
+
|
| 1077 |
+
gr.Markdown("---")
|
| 1078 |
+
gr.Markdown("## Strategy Management")
|
| 1079 |
+
|
| 1080 |
+
# Create a tabbed interface for strategy management
|
| 1081 |
+
with gr.Tabs():
|
| 1082 |
+
# Tab 1: Select Existing Strategy
|
| 1083 |
+
with gr.TabItem("Select Strategy"):
|
| 1084 |
+
gr.Markdown("### Available Strategies")
|
| 1085 |
+
|
| 1086 |
+
# Display saved strategies as a table
|
| 1087 |
+
saved_strategies = gr.Dataframe(
|
| 1088 |
+
headers=["ID", "Name", "Description"],
|
| 1089 |
+
datatype=["number", "str", "str"],
|
| 1090 |
+
label="Available Strategies"
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
refresh_strategies_btn = gr.Button("Refresh Strategies")
|
| 1094 |
+
|
| 1095 |
+
def get_strategies_as_df():
|
| 1096 |
+
strategies = get_saved_strategies()
|
| 1097 |
+
if not strategies:
|
| 1098 |
+
return [[0, "No strategies found", "Create a new strategy first"]]
|
| 1099 |
+
return [[s["id"], s["name"], s["description"]] for s in strategies]
|
| 1100 |
+
|
| 1101 |
+
refresh_strategies_btn.click(get_strategies_as_df, outputs=saved_strategies)
|
| 1102 |
+
|
| 1103 |
+
# Create a dropdown for strategy selection instead of number input
|
| 1104 |
+
gr.Markdown("### Select a Strategy")
|
| 1105 |
+
|
| 1106 |
+
def get_strategy_dropdown_choices():
|
| 1107 |
+
strategies = get_saved_strategies()
|
| 1108 |
+
if not strategies:
|
| 1109 |
+
return [("No strategies available", 0)]
|
| 1110 |
+
return [(f"{s['id']} - {s['name']}", s['id']) for s in strategies]
|
| 1111 |
+
|
| 1112 |
+
strategy_dropdown = gr.Dropdown(
|
| 1113 |
+
choices=get_strategy_dropdown_choices(),
|
| 1114 |
+
label="Choose Strategy",
|
| 1115 |
+
value=None
|
| 1116 |
+
)
|
| 1117 |
+
|
| 1118 |
+
# Connect refresh button to also update dropdown
|
| 1119 |
+
def refresh_all_strategy_displays():
|
| 1120 |
+
df = get_strategies_as_df()
|
| 1121 |
+
choices = get_strategy_dropdown_choices()
|
| 1122 |
+
return df, choices
|
| 1123 |
+
|
| 1124 |
+
refresh_strategies_btn.click(
|
| 1125 |
+
refresh_all_strategy_displays,
|
| 1126 |
+
outputs=[saved_strategies, strategy_dropdown]
|
| 1127 |
+
)
|
| 1128 |
+
|
| 1129 |
+
# Load, Delete buttons
|
| 1130 |
+
with gr.Row():
|
| 1131 |
+
load_btn = gr.Button("Load Selected Strategy", variant="primary")
|
| 1132 |
+
delete_btn = gr.Button("Delete Selected Strategy", variant="stop")
|
| 1133 |
+
|
| 1134 |
+
# Add a status message
|
| 1135 |
+
strategy_action_message = gr.Textbox(label="Status", interactive=False)
|
| 1136 |
+
|
| 1137 |
+
# Add strategy details display section
|
| 1138 |
+
gr.Markdown("### Selected Strategy Details")
|
| 1139 |
+
selected_strategy_view = gr.Markdown("No strategy selected")
|
| 1140 |
+
|
| 1141 |
+
# Update load strategy function to work with dropdown
|
| 1142 |
+
def load_selected_strategy(strategy_dropdown_value):
|
| 1143 |
+
if not strategy_dropdown_value:
|
| 1144 |
+
return "Please select a strategy first", "No strategy selected"
|
| 1145 |
+
|
| 1146 |
+
try:
|
| 1147 |
+
strategy_id = int(strategy_dropdown_value)
|
| 1148 |
+
strategy = get_strategy_by_id(strategy_id)
|
| 1149 |
+
|
| 1150 |
+
if not strategy:
|
| 1151 |
+
return "Strategy not found", "Strategy not found"
|
| 1152 |
+
|
| 1153 |
+
# Update global parameters
|
| 1154 |
+
global strategy_params
|
| 1155 |
+
strategy_params["strategy_text"] = strategy["strategy_text"]
|
| 1156 |
+
for key, value in strategy["parameters"].items():
|
| 1157 |
+
if key in strategy_params:
|
| 1158 |
+
strategy_params[key] = value
|
| 1159 |
+
|
| 1160 |
+
# Create a markdown view of the strategy - improved formatting
|
| 1161 |
+
view_text = f"""
|
| 1162 |
+
## {strategy['name']}
|
| 1163 |
+
|
| 1164 |
+
**Description**: {strategy['description']}
|
| 1165 |
+
|
| 1166 |
+
**Strategy Logic**:
|
| 1167 |
+
```
|
| 1168 |
+
{strategy['strategy_text']}
|
| 1169 |
+
```
|
| 1170 |
+
|
| 1171 |
+
**Parameters**:
|
| 1172 |
+
- Timeframe: {strategy["parameters"].get("timeframe_minutes", "N/A")} minutes
|
| 1173 |
+
- Max Allocation: {strategy["parameters"].get("max_allocation_percentage", "N/A")}%
|
| 1174 |
+
"""
|
| 1175 |
+
|
| 1176 |
+
# Return success message and view
|
| 1177 |
+
return f"Strategy '{strategy['name']}' loaded successfully", view_text
|
| 1178 |
+
except Exception as e:
|
| 1179 |
+
return f"Error loading strategy: {e}", "Error loading strategy"
|
| 1180 |
+
|
| 1181 |
+
# Delete strategy function updated for dropdown
|
| 1182 |
+
def delete_selected_strategy(strategy_dropdown_value):
|
| 1183 |
+
if not strategy_dropdown_value:
|
| 1184 |
+
return "Please select a strategy first", get_strategy_dropdown_choices()
|
| 1185 |
+
|
| 1186 |
+
try:
|
| 1187 |
+
strategy_id = int(strategy_dropdown_value)
|
| 1188 |
+
conn = sqlite3.connect(DB_PATH)
|
| 1189 |
+
cursor = conn.cursor()
|
| 1190 |
+
|
| 1191 |
+
# Check if strategy exists
|
| 1192 |
+
cursor.execute("SELECT name FROM strategies WHERE id = ?", (strategy_id,))
|
| 1193 |
+
result = cursor.fetchone()
|
| 1194 |
+
|
| 1195 |
+
if not result:
|
| 1196 |
+
conn.close()
|
| 1197 |
+
return "Strategy not found", get_strategy_dropdown_choices()
|
| 1198 |
+
|
| 1199 |
+
strategy_name = result[0]
|
| 1200 |
+
|
| 1201 |
+
# Delete the strategy
|
| 1202 |
+
cursor.execute("DELETE FROM strategies WHERE id = ?", (strategy_id,))
|
| 1203 |
+
conn.commit()
|
| 1204 |
+
conn.close()
|
| 1205 |
+
|
| 1206 |
+
# Get updated choices
|
| 1207 |
+
new_choices = get_strategy_dropdown_choices()
|
| 1208 |
+
|
| 1209 |
+
return f"Strategy '{strategy_name}' deleted successfully", new_choices
|
| 1210 |
+
except Exception as e:
|
| 1211 |
+
return f"Error deleting strategy: {e}", get_strategy_dropdown_choices()
|
| 1212 |
+
|
| 1213 |
+
# Connect buttons to new functions
|
| 1214 |
+
load_btn.click(
|
| 1215 |
+
load_selected_strategy,
|
| 1216 |
+
inputs=[strategy_dropdown],
|
| 1217 |
+
outputs=[strategy_action_message, selected_strategy_view]
|
| 1218 |
+
)
|
| 1219 |
+
|
| 1220 |
+
delete_btn.click(
|
| 1221 |
+
delete_selected_strategy,
|
| 1222 |
+
inputs=[strategy_dropdown],
|
| 1223 |
+
outputs=[strategy_action_message, strategy_dropdown]
|
| 1224 |
+
)
|
| 1225 |
+
|
| 1226 |
+
# Tab 2: Create/Edit Strategy
|
| 1227 |
+
with gr.TabItem("Create/Edit Strategy"):
|
| 1228 |
+
gr.Markdown("### Strategy Editor")
|
| 1229 |
+
|
| 1230 |
+
# Strategy editor section
|
| 1231 |
+
strategy_name = gr.Textbox(label="Strategy Name", placeholder="Enter a name for your strategy")
|
| 1232 |
+
strategy_description = gr.Textbox(label="Strategy Description", placeholder="Enter a brief description of what your strategy does")
|
| 1233 |
+
|
| 1234 |
+
# Add a checkbox to indicate if editing existing strategy
|
| 1235 |
+
is_editing = gr.Checkbox(label="Edit Existing Strategy", value=False)
|
| 1236 |
+
edit_id = gr.Number(label="Strategy ID to Edit", value=0, visible=False)
|
| 1237 |
+
|
| 1238 |
+
# Add a dropdown to select strategy for editing
|
| 1239 |
+
edit_strategy_dropdown = gr.Dropdown(
|
| 1240 |
+
choices=get_strategy_dropdown_choices(),
|
| 1241 |
+
label="Choose Strategy to Edit",
|
| 1242 |
+
visible=False
|
| 1243 |
+
)
|
| 1244 |
+
|
| 1245 |
+
gr.Markdown("### Strategy Logic")
|
| 1246 |
+
gr.Markdown("""Write your trading strategy using the available indicators. Be specific about entry and exit conditions.
|
| 1247 |
+
|
| 1248 |
+
**Example Strategy:**
|
| 1249 |
+
```
|
| 1250 |
+
Buy when RSI is below 30 and the price is near the lower Bollinger Band (position < 0.2).
|
| 1251 |
+
Sell when RSI is above 70 and the price is near the upper Bollinger Band (position > 0.8).
|
| 1252 |
+
Increase confidence if the ADX shows a strong trend (> 25) in the same direction.
|
| 1253 |
+
Use 40% of available capital for trades if confidence is high (> 70), otherwise use 20%.
|
| 1254 |
+
```
|
| 1255 |
+
|
| 1256 |
+
The strategy will be interpreted by AI to generate trading signals based on current market conditions.""")
|
| 1257 |
+
|
| 1258 |
+
# Available indicators
|
| 1259 |
+
with gr.Accordion("View Available Indicators", open=False):
|
| 1260 |
+
indicators_info = gr.Markdown(get_available_indicators())
|
| 1261 |
+
|
| 1262 |
+
# Strategy text editor - moved up before being referenced
|
| 1263 |
+
strategy_text = gr.TextArea(
|
| 1264 |
+
label="Strategy Logic",
|
| 1265 |
+
placeholder="Enter your trading strategy logic here...",
|
| 1266 |
+
value=strategy_params.get("strategy_text", ""),
|
| 1267 |
+
lines=10,
|
| 1268 |
+
max_lines=20
|
| 1269 |
+
)
|
| 1270 |
+
|
| 1271 |
+
# Connect checkbox to show/hide controls
|
| 1272 |
+
def toggle_edit_mode(is_editing):
|
| 1273 |
+
return {
|
| 1274 |
+
edit_strategy_dropdown: gr.update(visible=is_editing),
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
is_editing.change(
|
| 1278 |
+
toggle_edit_mode,
|
| 1279 |
+
inputs=[is_editing],
|
| 1280 |
+
outputs=[edit_strategy_dropdown]
|
| 1281 |
+
)
|
| 1282 |
+
|
| 1283 |
+
# Function to load strategy into editor
|
| 1284 |
+
def load_strategy_to_editor(strategy_dropdown_value):
|
| 1285 |
+
if not strategy_dropdown_value:
|
| 1286 |
+
return "Please select a strategy to edit", "", "", "", 0
|
| 1287 |
+
|
| 1288 |
+
try:
|
| 1289 |
+
strategy_id = int(strategy_dropdown_value)
|
| 1290 |
+
strategy = get_strategy_by_id(strategy_id)
|
| 1291 |
+
|
| 1292 |
+
if not strategy:
|
| 1293 |
+
return "Strategy not found", "", "", "", 0
|
| 1294 |
+
|
| 1295 |
+
return "Strategy loaded for editing", strategy["name"], strategy["description"], strategy["strategy_text"], strategy_id
|
| 1296 |
+
except Exception as e:
|
| 1297 |
+
return f"Error loading strategy: {e}", "", "", "", 0
|
| 1298 |
+
|
| 1299 |
+
# Connect edit dropdown to load function
|
| 1300 |
+
edit_strategy_dropdown.change(
|
| 1301 |
+
load_strategy_to_editor,
|
| 1302 |
+
inputs=[edit_strategy_dropdown],
|
| 1303 |
+
outputs=[strategy_action_message, strategy_name, strategy_description, strategy_text, edit_id]
|
| 1304 |
+
)
|
| 1305 |
+
|
| 1306 |
+
# Save button and status
|
| 1307 |
+
save_btn = gr.Button("Save Strategy", variant="primary")
|
| 1308 |
+
save_status = gr.Textbox(label="Save Status", interactive=False)
|
| 1309 |
+
|
| 1310 |
+
# Enhanced save function
|
| 1311 |
+
def save_strategy_enhanced(is_editing, edit_id, name, description, text):
|
| 1312 |
+
try:
|
| 1313 |
+
if not name or not text:
|
| 1314 |
+
return "Error: Strategy name and logic are required", get_strategy_dropdown_choices(), get_strategy_dropdown_choices()
|
| 1315 |
+
|
| 1316 |
+
# Get current strategy parameters
|
| 1317 |
+
parameters = strategy_params.copy()
|
| 1318 |
+
parameters["strategy_text"] = text
|
| 1319 |
+
|
| 1320 |
+
conn = sqlite3.connect(DB_PATH)
|
| 1321 |
+
cursor = conn.cursor()
|
| 1322 |
+
|
| 1323 |
+
strategy_id = int(edit_id) if is_editing and edit_id > 0 else 0
|
| 1324 |
+
|
| 1325 |
+
if is_editing and strategy_id > 0:
|
| 1326 |
+
# Update existing strategy
|
| 1327 |
+
cursor.execute(
|
| 1328 |
+
"UPDATE strategies SET name = ?, description = ?, strategy_text = ?, parameters = ? WHERE id = ?",
|
| 1329 |
+
(name, description, text, json.dumps(parameters), strategy_id)
|
| 1330 |
+
)
|
| 1331 |
+
message = f"Strategy '{name}' updated successfully"
|
| 1332 |
+
else:
|
| 1333 |
+
# Create new strategy
|
| 1334 |
+
cursor.execute(
|
| 1335 |
+
"INSERT INTO strategies (name, description, strategy_text, parameters) VALUES (?, ?, ?, ?)",
|
| 1336 |
+
(name, description, text, json.dumps(parameters))
|
| 1337 |
+
)
|
| 1338 |
+
message = f"Strategy '{name}' saved successfully"
|
| 1339 |
+
|
| 1340 |
+
conn.commit()
|
| 1341 |
+
conn.close()
|
| 1342 |
+
|
| 1343 |
+
# Update dropdown choices
|
| 1344 |
+
new_choices = get_strategy_dropdown_choices()
|
| 1345 |
+
|
| 1346 |
+
return message, new_choices, new_choices
|
| 1347 |
+
except Exception as e:
|
| 1348 |
+
return f"Error saving strategy: {e}", get_strategy_dropdown_choices(), get_strategy_dropdown_choices()
|
| 1349 |
+
|
| 1350 |
+
# Connect save button
|
| 1351 |
+
save_btn.click(
|
| 1352 |
+
save_strategy_enhanced,
|
| 1353 |
+
inputs=[is_editing, edit_id, strategy_name, strategy_description, strategy_text],
|
| 1354 |
+
outputs=[save_status, strategy_dropdown, edit_strategy_dropdown]
|
| 1355 |
+
)
|
| 1356 |
+
|
| 1357 |
+
# Clear form button
|
| 1358 |
+
clear_btn = gr.Button("Clear Form")
|
| 1359 |
+
|
| 1360 |
+
def clear_form():
|
| 1361 |
+
return "", "", "", False, 0
|
| 1362 |
+
|
| 1363 |
+
clear_btn.click(
|
| 1364 |
+
clear_form,
|
| 1365 |
+
outputs=[strategy_name, strategy_description, strategy_text, is_editing, edit_id]
|
| 1366 |
+
)
|
| 1367 |
+
|
| 1368 |
+
gr.Markdown("---")
|
| 1369 |
+
gr.Markdown("## Execute Analysis")
|
| 1370 |
+
gr.Markdown("First select a strategy above, then run the analysis pipeline.")
|
| 1371 |
+
|
| 1372 |
+
# Check if strategy is loaded - improved formatting
|
| 1373 |
+
def check_strategy_loaded():
|
| 1374 |
+
if not strategy_params.get("strategy_text"):
|
| 1375 |
+
return (
|
| 1376 |
+
"β οΈ No strategy selected! Please load a strategy first.",
|
| 1377 |
+
gr.update(interactive=False)
|
| 1378 |
+
)
|
| 1379 |
+
else:
|
| 1380 |
+
# Format the strategy preview in a more structured way
|
| 1381 |
+
strategy_text = strategy_params.get("strategy_text", "")
|
| 1382 |
+
|
| 1383 |
+
# Properly format the strategy preview
|
| 1384 |
+
strategy_preview = f"""
|
| 1385 |
+
## Current Strategy
|
| 1386 |
+
|
| 1387 |
+
```
|
| 1388 |
+
{strategy_text}
|
| 1389 |
+
```
|
| 1390 |
+
|
| 1391 |
+
**Parameters**:
|
| 1392 |
+
- Timeframe: {strategy_params.get('timeframe_minutes', 'N/A')} minutes
|
| 1393 |
+
- Max Allocation: {strategy_params.get('max_allocation_percentage', 'N/A')}%
|
| 1394 |
+
"""
|
| 1395 |
+
|
| 1396 |
+
return (
|
| 1397 |
+
strategy_preview,
|
| 1398 |
+
gr.update(interactive=True)
|
| 1399 |
+
)
|
| 1400 |
+
|
| 1401 |
+
# Display current strategy and run button
|
| 1402 |
+
current_strategy_info = gr.Markdown("No strategy loaded")
|
| 1403 |
+
run_pipeline_btn = gr.Button("Run Analysis Pipeline", variant="primary", interactive=False)
|
| 1404 |
+
|
| 1405 |
+
# Add a refresh button to check if strategy is loaded
|
| 1406 |
+
check_strategy_btn = gr.Button("Check Current Strategy")
|
| 1407 |
+
check_strategy_btn.click(
|
| 1408 |
+
check_strategy_loaded,
|
| 1409 |
+
outputs=[current_strategy_info, run_pipeline_btn]
|
| 1410 |
+
)
|
| 1411 |
+
|
| 1412 |
+
# Run button should automatically check if strategy is loaded
|
| 1413 |
+
analysis_result = gr.Markdown("Analysis results will appear here...")
|
| 1414 |
+
|
| 1415 |
+
# Function to run full analysis and format detailed output
|
| 1416 |
+
def run_full_analysis_with_check():
|
| 1417 |
+
try:
|
| 1418 |
+
if not strategy_params.get("strategy_text"):
|
| 1419 |
+
return "Error: No strategy selected. Please load a strategy before running analysis."
|
| 1420 |
+
|
| 1421 |
+
result = run_full_analysis()
|
| 1422 |
+
if "error" in result:
|
| 1423 |
+
return f"Analysis failed: {result['error']}"
|
| 1424 |
+
|
| 1425 |
+
return format_detailed_analysis_results(result)
|
| 1426 |
+
except Exception as e:
|
| 1427 |
+
return f"Error running analysis: {str(e)}"
|
| 1428 |
+
|
| 1429 |
+
# Connect the run button
|
| 1430 |
+
run_pipeline_btn.click(
|
| 1431 |
+
run_full_analysis_with_check,
|
| 1432 |
+
outputs=[analysis_result]
|
| 1433 |
+
)
|
| 1434 |
+
|
| 1435 |
+
# Trading Tab
|
| 1436 |
+
with gr.TabItem("Trading"):
|
| 1437 |
+
gr.Markdown("## Account Summary")
|
| 1438 |
+
|
| 1439 |
+
account_summary = gr.Markdown("")
|
| 1440 |
+
|
| 1441 |
+
with gr.Row():
|
| 1442 |
+
refresh_account_btn = gr.Button("Refresh Account Info")
|
| 1443 |
+
reset_portfolio_btn = gr.Button("Reset Portfolio")
|
| 1444 |
+
|
| 1445 |
+
reset_portfolio_message = gr.Textbox(label="Reset Portfolio Status")
|
| 1446 |
+
|
| 1447 |
+
refresh_account_btn.click(get_account_summary, outputs=account_summary)
|
| 1448 |
+
reset_portfolio_btn.click(reset_portfolio, outputs=reset_portfolio_message)
|
| 1449 |
+
|
| 1450 |
+
gr.Markdown("## Manual Trading")
|
| 1451 |
+
|
| 1452 |
+
with gr.Row():
|
| 1453 |
+
action = gr.Dropdown(["buy", "sell", "check"], label="Action")
|
| 1454 |
+
symbol = gr.Dropdown(["BTC/USD", "ETH/USD"], label="Symbol", value="BTC/USD")
|
| 1455 |
+
allocation = gr.Slider(minimum=1, maximum=100, value=10, label="Allocation Percentage")
|
| 1456 |
+
|
| 1457 |
+
execute_btn = gr.Button("Execute Trade")
|
| 1458 |
+
trade_result = gr.Textbox(label="Trade Result")
|
| 1459 |
+
|
| 1460 |
+
execute_btn.click(execute_trade, inputs=[action, symbol, allocation], outputs=trade_result)
|
| 1461 |
+
|
| 1462 |
+
# Monitoring Tab
|
| 1463 |
+
with gr.TabItem("Monitoring"):
|
| 1464 |
+
with gr.Tabs():
|
| 1465 |
+
with gr.TabItem("Analysis Results"):
|
| 1466 |
+
gr.Markdown("## Latest Analysis Results")
|
| 1467 |
+
|
| 1468 |
+
analysis_history = gr.Dataframe(
|
| 1469 |
+
headers=["ID", "Timestamp", "Strategy", "Signal", "Confidence", "Allocation"],
|
| 1470 |
+
datatype=["number", "str", "str", "str", "number", "number"],
|
| 1471 |
+
label="Recent Analysis Results"
|
| 1472 |
+
)
|
| 1473 |
+
|
| 1474 |
+
def get_analysis_history():
|
| 1475 |
+
results = get_recent_analysis_results(10)
|
| 1476 |
+
return [
|
| 1477 |
+
[r["id"], r["timestamp"], r["strategy_name"], r["signal"].upper(),
|
| 1478 |
+
r["confidence"], r["allocation_percentage"]]
|
| 1479 |
+
for r in results
|
| 1480 |
+
]
|
| 1481 |
+
|
| 1482 |
+
refresh_analysis_btn = gr.Button("Refresh Results")
|
| 1483 |
+
refresh_analysis_btn.click(get_analysis_history, outputs=analysis_history)
|
| 1484 |
+
|
| 1485 |
+
selected_analysis_id = gr.Number(label="Result ID to View", value=0)
|
| 1486 |
+
view_analysis_btn = gr.Button("View Detailed Analysis")
|
| 1487 |
+
|
| 1488 |
+
detailed_analysis = gr.Markdown("Select an analysis result and click 'View Detailed Analysis'")
|
| 1489 |
+
|
| 1490 |
+
def view_detailed_analysis(analysis_id):
|
| 1491 |
+
if analysis_id <= 0:
|
| 1492 |
+
return "Please select a valid analysis result ID"
|
| 1493 |
+
|
| 1494 |
+
try:
|
| 1495 |
+
conn = sqlite3.connect(DB_PATH)
|
| 1496 |
+
cursor = conn.cursor()
|
| 1497 |
+
|
| 1498 |
+
# Get the full analysis result
|
| 1499 |
+
cursor.execute("""
|
| 1500 |
+
SELECT ar.*, s.name as strategy_name
|
| 1501 |
+
FROM analysis_results ar
|
| 1502 |
+
LEFT JOIN strategies s ON ar.strategy_id = s.id
|
| 1503 |
+
WHERE ar.id = ?
|
| 1504 |
+
""", (int(analysis_id),))
|
| 1505 |
+
|
| 1506 |
+
row = cursor.fetchone()
|
| 1507 |
+
conn.close()
|
| 1508 |
+
|
| 1509 |
+
if not row:
|
| 1510 |
+
return "Analysis result not found"
|
| 1511 |
+
|
| 1512 |
+
# Convert row to dict
|
| 1513 |
+
column_names = [description[0] for description in cursor.description]
|
| 1514 |
+
result = {column_names[i]: row[i] for i in range(len(column_names))}
|
| 1515 |
+
|
| 1516 |
+
# Parse JSON fields
|
| 1517 |
+
if "indicator_values" in result and result["indicator_values"]:
|
| 1518 |
+
try:
|
| 1519 |
+
result["technical_indicators"] = json.loads(result["indicator_values"])
|
| 1520 |
+
except json.JSONDecodeError as e:
|
| 1521 |
+
result["technical_indicators"] = {"error": f"Failed to parse indicators: {str(e)}"}
|
| 1522 |
+
|
| 1523 |
+
# Format the detailed result
|
| 1524 |
+
return format_detailed_analysis_results(result)
|
| 1525 |
+
except Exception as e:
|
| 1526 |
+
import traceback
|
| 1527 |
+
return f"Error retrieving analysis details: {str(e)}\n{traceback.format_exc()}"
|
| 1528 |
+
|
| 1529 |
+
view_analysis_btn.click(view_detailed_analysis, inputs=[selected_analysis_id], outputs=detailed_analysis)
|
| 1530 |
+
|
| 1531 |
+
with gr.TabItem("Positions"):
|
| 1532 |
+
gr.Markdown("## Active Positions")
|
| 1533 |
+
|
| 1534 |
+
positions_display = gr.Markdown("")
|
| 1535 |
+
refresh_positions_btn = gr.Button("Refresh Positions")
|
| 1536 |
+
|
| 1537 |
+
refresh_positions_btn.click(format_active_positions, outputs=positions_display)
|
| 1538 |
+
|
| 1539 |
+
with gr.TabItem("Transactions"):
|
| 1540 |
+
gr.Markdown("## Transaction History")
|
| 1541 |
+
|
| 1542 |
+
transactions_history = gr.Dataframe(
|
| 1543 |
+
headers=["ID", "Date", "Symbol", "Action", "Quantity", "Price", "Status"],
|
| 1544 |
+
datatype=["number", "str", "str", "str", "number", "number", "str"],
|
| 1545 |
+
label="Recent Transactions"
|
| 1546 |
+
)
|
| 1547 |
+
|
| 1548 |
+
def get_transactions_history():
|
| 1549 |
+
txs = get_recent_transactions(20)
|
| 1550 |
+
return [
|
| 1551 |
+
[tx["id"], tx["timestamp"], tx["symbol"], tx["action"].upper(),
|
| 1552 |
+
float(tx["quantity"]), float(tx["price"]), tx["status"]]
|
| 1553 |
+
for tx in txs
|
| 1554 |
+
]
|
| 1555 |
+
|
| 1556 |
+
refresh_tx_btn = gr.Button("Refresh Transactions")
|
| 1557 |
+
refresh_tx_btn.click(get_transactions_history, outputs=transactions_history)
|
| 1558 |
+
|
| 1559 |
+
orders_display = gr.Markdown("")
|
| 1560 |
+
refresh_orders_btn = gr.Button("Show Full Transaction Details")
|
| 1561 |
+
|
| 1562 |
+
refresh_orders_btn.click(format_order_history, outputs=orders_display)
|
| 1563 |
+
|
| 1564 |
+
# Automated Analysis Tab
|
| 1565 |
+
with gr.TabItem("Automated Analysis"):
|
| 1566 |
+
gr.Markdown("## Background Analysis")
|
| 1567 |
+
|
| 1568 |
+
gr.Markdown("""
|
| 1569 |
+
### Full Crew Automated Analysis
|
| 1570 |
+
|
| 1571 |
+
The automated analysis will run the complete Bitcoin Analysis Crew at intervals defined by the timeframe setting.
|
| 1572 |
+
This includes:
|
| 1573 |
+
- Technical analysis
|
| 1574 |
+
- Market sentiment analysis
|
| 1575 |
+
- Order execution
|
| 1576 |
+
|
| 1577 |
+
**All trades and analysis results will be logged** to a file in the data directory for later review.
|
| 1578 |
+
""")
|
| 1579 |
+
|
| 1580 |
+
gr.Markdown(f"Current timeframe: **{strategy_params['timeframe_minutes']} minutes**")
|
| 1581 |
+
|
| 1582 |
+
with gr.Row():
|
| 1583 |
+
start_btn = gr.Button("Start Automated Analysis", variant="primary")
|
| 1584 |
+
stop_btn = gr.Button("Stop Automated Analysis", variant="stop")
|
| 1585 |
+
|
| 1586 |
+
auto_status = gr.Textbox(label="Automation Status")
|
| 1587 |
+
|
| 1588 |
+
start_btn.click(start_background_process, outputs=auto_status)
|
| 1589 |
+
stop_btn.click(stop_background_process, outputs=auto_status)
|
| 1590 |
+
|
| 1591 |
+
# Add log path information
|
| 1592 |
+
log_path_info = gr.Markdown(f"""
|
| 1593 |
+
**Log File Location**
|
| 1594 |
+
|
| 1595 |
+
Trading logs will be saved to:
|
| 1596 |
+
```
|
| 1597 |
+
{os.path.dirname(DB_PATH)}
|
| 1598 |
+
```
|
| 1599 |
+
A new log file is created for each trading session with format: `auto_trading_log_YYYYMMDD_HHMMSS.txt`
|
| 1600 |
+
|
| 1601 |
+
These logs contain:
|
| 1602 |
+
- All analysis results
|
| 1603 |
+
- Trade executions
|
| 1604 |
+
- Account balances
|
| 1605 |
+
- Profit/loss tracking
|
| 1606 |
+
- Errors and warnings
|
| 1607 |
+
""")
|
| 1608 |
+
|
| 1609 |
+
auto_result = gr.Markdown("Automated analysis results will appear here...")
|
| 1610 |
+
|
| 1611 |
+
# Refresh button with detailed results
|
| 1612 |
+
auto_refresh = gr.Button("Refresh Latest Results")
|
| 1613 |
+
|
| 1614 |
+
# Update to use detailed formatting
|
| 1615 |
+
auto_refresh.click(format_detailed_analysis_results, outputs=auto_result)
|
| 1616 |
+
|
| 1617 |
+
# Add warning about strategy selection
|
| 1618 |
+
gr.Markdown("""
|
| 1619 |
+
**IMPORTANT**: Make sure you have loaded a strategy from the Strategy Configuration tab before starting automated analysis.
|
| 1620 |
+
The system will use the currently loaded strategy for all automated runs.
|
| 1621 |
+
""")
|
| 1622 |
+
|
| 1623 |
+
# Add instructions for running for days
|
| 1624 |
+
gr.Markdown("""
|
| 1625 |
+
### Running for Extended Periods
|
| 1626 |
+
|
| 1627 |
+
To run this application continuously for days:
|
| 1628 |
+
|
| 1629 |
+
1. Start the automated analysis with your chosen strategy and timeframe
|
| 1630 |
+
2. Keep this application running (do not close the browser tab or terminal)
|
| 1631 |
+
3. If running on a remote server, use tools like `screen` or `tmux` to keep the session alive
|
| 1632 |
+
|
| 1633 |
+
**Example terminal command for persistent session**:
|
| 1634 |
+
```
|
| 1635 |
+
# Start a new screen session
|
| 1636 |
+
screen -S cryptic_trading
|
| 1637 |
+
|
| 1638 |
+
# Then run your application in that session
|
| 1639 |
+
python src/ui/app.py
|
| 1640 |
+
|
| 1641 |
+
# You can detach from the session with Ctrl+A, D and reconnect later with:
|
| 1642 |
+
screen -r cryptic_trading
|
| 1643 |
+
```
|
| 1644 |
+
""")
|
| 1645 |
+
|
| 1646 |
+
# Launch the app
|
| 1647 |
+
if __name__ == "__main__":
|
| 1648 |
+
# Ensure directories exist
|
| 1649 |
+
os.makedirs("results", exist_ok=True)
|
| 1650 |
+
|
| 1651 |
+
# Run initial data fetching
|
| 1652 |
+
fetch_account_info()
|
| 1653 |
+
fetch_order_history()
|
| 1654 |
+
fetch_active_positions()
|
| 1655 |
+
|
| 1656 |
+
# Launch the app
|
| 1657 |
+
app.launch(share=False)
|
test_crew.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.crypto_analysis.crew import BitcoinAnalysisCrew
|
| 2 |
+
|
| 3 |
+
# Create the crew with custom timeframe and max_allocation
|
| 4 |
+
crew = BitcoinAnalysisCrew(timeframe='15m', max_allocation=50)
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
# Run analysis
|
| 8 |
+
print("Starting analysis...")
|
| 9 |
+
result = crew.run_analysis()
|
| 10 |
+
print(f"Analysis completed with signal: {result.get('signal')}, "
|
| 11 |
+
f"confidence: {result.get('confidence')}%, "
|
| 12 |
+
f"allocation: {result.get('allocation_percentage')}%")
|
| 13 |
+
except Exception as e:
|
| 14 |
+
print(f"Error during analysis: {str(e)}")
|
tests/run_tests.sh
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Run all tests for the CrypticAI project
|
| 3 |
+
|
| 4 |
+
# Change to the project root directory
|
| 5 |
+
cd "$(dirname "$0")/.." || exit
|
| 6 |
+
|
| 7 |
+
# Setup
|
| 8 |
+
echo "========================================"
|
| 9 |
+
echo "Setting up environment..."
|
| 10 |
+
echo "========================================"
|
| 11 |
+
source venv/bin/activate || { echo "Failed to activate virtual environment. Make sure venv exists."; exit 1; }
|
| 12 |
+
|
| 13 |
+
# Run tools tests
|
| 14 |
+
echo -e "\n========================================"
|
| 15 |
+
echo "Running comprehensive tool tests..."
|
| 16 |
+
echo "========================================"
|
| 17 |
+
python tests/test_all_tools.py
|
| 18 |
+
|
| 19 |
+
# Run individual component tests
|
| 20 |
+
echo -e "\n========================================"
|
| 21 |
+
echo "Running Bitcoin Data Tool tests..."
|
| 22 |
+
echo "========================================"
|
| 23 |
+
python tests/test_bitcoin.py
|
| 24 |
+
|
| 25 |
+
echo -e "\n========================================"
|
| 26 |
+
echo "Running Bitcoin News Tool tests..."
|
| 27 |
+
echo "========================================"
|
| 28 |
+
python tests/test_bitcoin_news.py
|
| 29 |
+
|
| 30 |
+
echo -e "\n========================================"
|
| 31 |
+
echo "Running Yahoo Finance Tools tests..."
|
| 32 |
+
echo "========================================"
|
| 33 |
+
python tests/test_yahoo_tools.py
|
| 34 |
+
|
| 35 |
+
echo -e "\n========================================"
|
| 36 |
+
echo "All tests completed!"
|
| 37 |
+
echo "========================================"
|
tests/test_all_tools.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script to verify that tools can be initialized properly.
|
| 3 |
+
This is helpful for checking API keys and connections.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
from src.crypto_analysis.tools import (
|
| 11 |
+
AlpacaBitcoinDataTool,
|
| 12 |
+
TechnicalIndicatorsTool,
|
| 13 |
+
BitcoinNewsTool,
|
| 14 |
+
BitcoinSentimentTool,
|
| 15 |
+
YahooBitcoinDataTool,
|
| 16 |
+
YahooCryptoMarketTool
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def test_alpaca_tools():
|
| 20 |
+
"""Test Alpaca tools initialization and basic functionality"""
|
| 21 |
+
print("Testing Alpaca Bitcoin Data Tool...")
|
| 22 |
+
|
| 23 |
+
# Check if API keys are set
|
| 24 |
+
api_key = os.getenv("ALPACA_API_KEY")
|
| 25 |
+
api_secret = os.getenv("ALPACA_API_SECRET")
|
| 26 |
+
|
| 27 |
+
if not api_key or not api_secret:
|
| 28 |
+
print("β οΈ Alpaca API keys not found in environment variables")
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# Initialize the tool
|
| 33 |
+
tool = AlpacaBitcoinDataTool()
|
| 34 |
+
print("β Tool initialized successfully")
|
| 35 |
+
|
| 36 |
+
# Test a simple data fetch
|
| 37 |
+
print("Fetching data (this might take a few seconds)...")
|
| 38 |
+
result = tool._run(timeframe="5Min", days_back=1)
|
| 39 |
+
|
| 40 |
+
if "error" in result:
|
| 41 |
+
print(f"β οΈ Error: {result['error']}")
|
| 42 |
+
else:
|
| 43 |
+
print(f"β Successfully retrieved {len(result.get('dataframe', []))} data points")
|
| 44 |
+
print(f"β Last Bitcoin price: ${result.get('last_price', 'N/A')}")
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 48 |
+
|
| 49 |
+
def test_technical_indicators():
|
| 50 |
+
"""Test technical indicators tool"""
|
| 51 |
+
print("\nTesting Technical Indicators Tool...")
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
# Initialize the tool
|
| 55 |
+
tool = TechnicalIndicatorsTool()
|
| 56 |
+
print("β Tool initialized successfully")
|
| 57 |
+
|
| 58 |
+
# Test indicators calculation
|
| 59 |
+
print("Calculating indicators (this might take a few seconds)...")
|
| 60 |
+
result = tool._run(timeframe="5Min", days_back=1)
|
| 61 |
+
|
| 62 |
+
if "error" in result:
|
| 63 |
+
print(f"β οΈ Error: {result['error']}")
|
| 64 |
+
else:
|
| 65 |
+
print(f"β Successfully calculated technical indicators")
|
| 66 |
+
print(f"β Current RSI: {result.get('indicators', {}).get('rsi', 'N/A')}")
|
| 67 |
+
print(f"β Current ADX: {result.get('indicators', {}).get('adx', 'N/A')}")
|
| 68 |
+
print(f"β Buy signal: {result.get('signals', {}).get('buy_signal', 'N/A')}")
|
| 69 |
+
print(f"β Sell signal: {result.get('signals', {}).get('sell_signal', 'N/A')}")
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 73 |
+
|
| 74 |
+
def test_news_tools():
|
| 75 |
+
"""Test news tools initialization and basic functionality using Tavily"""
|
| 76 |
+
print("\nTesting Bitcoin News Tool with Tavily...")
|
| 77 |
+
|
| 78 |
+
# Check if API key is set
|
| 79 |
+
api_key = os.getenv("TAVILY_API_KEY")
|
| 80 |
+
|
| 81 |
+
if not api_key:
|
| 82 |
+
print("β οΈ Tavily API key not found in environment variables")
|
| 83 |
+
print("Will try to fetch news using web search as fallback")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
# Initialize the tool
|
| 87 |
+
tool = BitcoinNewsTool()
|
| 88 |
+
print("β Tool initialized successfully")
|
| 89 |
+
|
| 90 |
+
# Test news fetch
|
| 91 |
+
print("Fetching news (this might take a few seconds)...")
|
| 92 |
+
result = tool._run(days_back=1, limit=3)
|
| 93 |
+
|
| 94 |
+
if "error" in result:
|
| 95 |
+
print(f"β οΈ Error: {result['error']}")
|
| 96 |
+
else:
|
| 97 |
+
articles = result.get("articles", [])
|
| 98 |
+
print(f"β Successfully retrieved {len(articles)} news articles")
|
| 99 |
+
|
| 100 |
+
# Print article titles
|
| 101 |
+
for i, article in enumerate(articles, 1):
|
| 102 |
+
print(f" {i}. {article.get('title', 'No title')}")
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 106 |
+
|
| 107 |
+
def test_yahoo_tools():
|
| 108 |
+
"""Test Yahoo Finance tools"""
|
| 109 |
+
print("\nTesting Yahoo Finance Bitcoin Data Tool...")
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
# Initialize the tool
|
| 113 |
+
tool = YahooBitcoinDataTool()
|
| 114 |
+
print("β Tool initialized successfully")
|
| 115 |
+
|
| 116 |
+
# Test data fetch
|
| 117 |
+
print("Fetching data (this might take a few seconds)...")
|
| 118 |
+
result = tool._run(period="1d", interval="1h")
|
| 119 |
+
|
| 120 |
+
if "error" in result:
|
| 121 |
+
print(f"β οΈ Error: {result['error']}")
|
| 122 |
+
else:
|
| 123 |
+
print(f"β Successfully retrieved Bitcoin data from Yahoo Finance")
|
| 124 |
+
print(f"β Last price: ${result.get('last_price', 'N/A')}")
|
| 125 |
+
|
| 126 |
+
# Print metadata
|
| 127 |
+
metadata = result.get("metadata", {})
|
| 128 |
+
print(f"β Market cap: {metadata.get('market_cap', 'N/A')}")
|
| 129 |
+
print(f"β 24h volume: {metadata.get('volume_24h', 'N/A')}")
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"β οΈ Exception occurred: {e}")
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
print("=" * 50)
|
| 136 |
+
print("TESTING BITCOIN ANALYSIS TOOLS")
|
| 137 |
+
print("=" * 50)
|
| 138 |
+
|
| 139 |
+
# Test each tool category
|
| 140 |
+
test_yahoo_tools() # Start with Yahoo as it doesn't require API keys
|
| 141 |
+
test_alpaca_tools()
|
| 142 |
+
test_news_tools()
|
| 143 |
+
test_technical_indicators()
|
| 144 |
+
|
| 145 |
+
print("\n" + "=" * 50)
|
| 146 |
+
print("TESTING COMPLETE")
|
| 147 |
+
print("=" * 50)
|
tests/test_bitcoin.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple test script for Bitcoin analysis
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# Add the project root to the Python path
|
| 12 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
# Check required API keys
|
| 18 |
+
openai_key = os.getenv("OPENAI_API_KEY")
|
| 19 |
+
if not openai_key:
|
| 20 |
+
print("Error: OPENAI_API_KEY not found in environment variables")
|
| 21 |
+
sys.exit(1)
|
| 22 |
+
|
| 23 |
+
print("Testing Bitcoin tools...")
|
| 24 |
+
|
| 25 |
+
# Test Yahoo Bitcoin Data Tool
|
| 26 |
+
try:
|
| 27 |
+
from src.crypto_analysis.tools.bitcoin_tools import YahooBitcoinDataTool
|
| 28 |
+
|
| 29 |
+
print("\nTesting Yahoo Bitcoin Data Tool...")
|
| 30 |
+
bitcoin_data_tool = YahooBitcoinDataTool()
|
| 31 |
+
data = bitcoin_data_tool._run(period="1mo", interval="1d")
|
| 32 |
+
|
| 33 |
+
if "error" in data:
|
| 34 |
+
print(f"β Error: {data['error']}")
|
| 35 |
+
else:
|
| 36 |
+
print(f"β
Successfully retrieved Bitcoin data")
|
| 37 |
+
print(f"π Current price: ${data['price']:.2f}")
|
| 38 |
+
print(f"π Trend: {data['trend']} ({data['percent_change']:.2f}%)")
|
| 39 |
+
print(f"π° Market cap: {data['market_cap']}")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"β Error testing Bitcoin Data Tool: {str(e)}")
|
| 42 |
+
|
| 43 |
+
# Test Bitcoin News Tool
|
| 44 |
+
try:
|
| 45 |
+
from src.crypto_analysis.tools.bitcoin_tools import BitcoinNewsTool
|
| 46 |
+
|
| 47 |
+
print("\nTesting Bitcoin News Tool...")
|
| 48 |
+
bitcoin_news_tool = BitcoinNewsTool()
|
| 49 |
+
news = bitcoin_news_tool._run()
|
| 50 |
+
|
| 51 |
+
if news and "articles" in news and len(news["articles"]) > 0:
|
| 52 |
+
print(f"β
Successfully retrieved Bitcoin news")
|
| 53 |
+
for i, article in enumerate(news["articles"]):
|
| 54 |
+
print(f"π° Article {i+1}: {article['title']}")
|
| 55 |
+
else:
|
| 56 |
+
print("β No news articles found")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"β Error testing Bitcoin News Tool: {str(e)}")
|
| 59 |
+
|
| 60 |
+
# Test Bitcoin Analysis Crew
|
| 61 |
+
try:
|
| 62 |
+
from src.crypto_analysis.crew import BitcoinAnalysisCrew
|
| 63 |
+
|
| 64 |
+
print("\nTesting Bitcoin Analysis Crew...")
|
| 65 |
+
print("This may take a minute or two as it needs to query the LLM...")
|
| 66 |
+
|
| 67 |
+
crew = BitcoinAnalysisCrew()
|
| 68 |
+
result = crew.run_analysis()
|
| 69 |
+
|
| 70 |
+
print("\nAnalysis Results:")
|
| 71 |
+
# Don't try to JSON serialize the entire result
|
| 72 |
+
print(f"Signal: {result.get('signal', 'unknown')}")
|
| 73 |
+
print(f"Confidence: {result.get('confidence', 0)}%")
|
| 74 |
+
print(f"Reasoning: {result.get('reasoning', 'No reasoning provided')}")
|
| 75 |
+
|
| 76 |
+
print(f"\nβ
Analysis complete!")
|
| 77 |
+
print(f"π Recommendation: {result.get('signal', 'unknown').upper()}")
|
| 78 |
+
print(f"π― Confidence: {result.get('confidence', 0)}%")
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"β Error testing Bitcoin Analysis Crew: {str(e)}")
|
| 82 |
+
|
| 83 |
+
print("\nAll tests completed!")
|
tests/test_bitcoin_news.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for the RealBitcoinNewsTool
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from src.crypto_analysis.tools.bitcoin_tools import RealBitcoinNewsTool, YahooBitcoinDataTool
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
"""Test the Bitcoin news tool"""
|
| 11 |
+
print("===== Bitcoin Data Tool Test =====")
|
| 12 |
+
price_tool = YahooBitcoinDataTool()
|
| 13 |
+
price_data = price_tool._run()
|
| 14 |
+
print(f"Bitcoin Price: ${price_data.get('price', 'N/A')}")
|
| 15 |
+
print(f"Market Cap: ${price_data.get('market_cap', 'N/A')}")
|
| 16 |
+
print(f"Percent Change: {price_data.get('percent_change', 'N/A')}%")
|
| 17 |
+
print(f"Trend: {price_data.get('trend', 'N/A')}")
|
| 18 |
+
print()
|
| 19 |
+
|
| 20 |
+
print("===== Bitcoin News Tool Test =====")
|
| 21 |
+
news_tool = RealBitcoinNewsTool()
|
| 22 |
+
|
| 23 |
+
print("\nTesting with source=None (should try multiple sources):")
|
| 24 |
+
news_data = news_tool._run(count=3)
|
| 25 |
+
print(f"Found {news_data.get('count', 0)} articles")
|
| 26 |
+
|
| 27 |
+
# Print each article
|
| 28 |
+
for i, article in enumerate(news_data.get('articles', []), 1):
|
| 29 |
+
print(f"\nArticle {i}:")
|
| 30 |
+
print(f"Title: {article.get('title', 'N/A')}")
|
| 31 |
+
print(f"Source: {article.get('source', 'N/A')}")
|
| 32 |
+
print(f"URL: {article.get('url', 'N/A')}")
|
| 33 |
+
description = article.get('description', 'N/A')
|
| 34 |
+
print(f"Description: {description[:100]}..." if len(description) > 100 else f"Description: {description}")
|
| 35 |
+
|
| 36 |
+
# Now try with a specific source
|
| 37 |
+
print("\nTesting with source='coindesk':")
|
| 38 |
+
news_data = news_tool._run(source='coindesk', count=2)
|
| 39 |
+
print(f"Found {news_data.get('count', 0)} articles from CoinDesk")
|
| 40 |
+
|
| 41 |
+
# Print each article
|
| 42 |
+
for i, article in enumerate(news_data.get('articles', []), 1):
|
| 43 |
+
print(f"\nArticle {i}:")
|
| 44 |
+
print(f"Title: {article.get('title', 'N/A')}")
|
| 45 |
+
print(f"Source: {article.get('source', 'N/A')}")
|
| 46 |
+
print(f"URL: {article.get('url', 'N/A')}")
|
| 47 |
+
description = article.get('description', 'N/A')
|
| 48 |
+
print(f"Description: {description[:100]}..." if len(description) > 100 else f"Description: {description}")
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
main()
|
tests/test_yahoo_tools.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for the Yahoo Finance crypto tools
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# Add the project root to the Python path
|
| 12 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
"""Test the Yahoo Finance crypto tools"""
|
| 19 |
+
# Test Yahoo Bitcoin Data Tool
|
| 20 |
+
try:
|
| 21 |
+
from src.crypto_analysis.tools.yahoo_tools import YahooBitcoinDataTool
|
| 22 |
+
|
| 23 |
+
print("\n===== Yahoo Bitcoin Data Tool Test =====")
|
| 24 |
+
bitcoin_data_tool = YahooBitcoinDataTool()
|
| 25 |
+
data = bitcoin_data_tool._run(period="1d", interval="1h")
|
| 26 |
+
|
| 27 |
+
if "error" in data:
|
| 28 |
+
print(f"β Error: {data['error']}")
|
| 29 |
+
else:
|
| 30 |
+
print(f"β
Successfully retrieved Bitcoin data from Yahoo Finance")
|
| 31 |
+
print(f"π Current price: ${data.get('last_price', 'N/A')}")
|
| 32 |
+
print(f"π° Market cap: {data.get('metadata', {}).get('market_cap', 'N/A')}")
|
| 33 |
+
print(f"π Time period: {data.get('time_period', 'N/A')}")
|
| 34 |
+
print(f"β±οΈ Interval: {data.get('interval', 'N/A')}")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"β Error testing Yahoo Bitcoin Data Tool: {str(e)}")
|
| 37 |
+
|
| 38 |
+
# Test Yahoo Crypto Market Tool
|
| 39 |
+
try:
|
| 40 |
+
from src.crypto_analysis.tools.yahoo_tools import YahooCryptoMarketTool
|
| 41 |
+
|
| 42 |
+
print("\n===== Yahoo Crypto Market Tool Test =====")
|
| 43 |
+
crypto_market_tool = YahooCryptoMarketTool()
|
| 44 |
+
market_data = crypto_market_tool._run(top_n=5)
|
| 45 |
+
|
| 46 |
+
if "error" in market_data:
|
| 47 |
+
print(f"β Error: {market_data['error']}")
|
| 48 |
+
else:
|
| 49 |
+
print(f"β
Successfully retrieved crypto market data")
|
| 50 |
+
|
| 51 |
+
# Market summary
|
| 52 |
+
summary = market_data.get('market_summary', {})
|
| 53 |
+
print(f"π Total market cap: ${summary.get('total_market_cap', 'N/A')}")
|
| 54 |
+
print(f"πΆ BTC dominance: {summary.get('btc_dominance', 'N/A'):.2f}%")
|
| 55 |
+
print(f"π Market trend: {summary.get('market_trend', 'N/A')}")
|
| 56 |
+
|
| 57 |
+
# Top cryptocurrencies
|
| 58 |
+
cryptos = market_data.get('cryptocurrencies', [])
|
| 59 |
+
print(f"\nπ Top {len(cryptos)} cryptocurrencies:")
|
| 60 |
+
|
| 61 |
+
for i, crypto in enumerate(cryptos, 1):
|
| 62 |
+
print(f"\n{i}. {crypto.get('name', crypto.get('ticker', 'Unknown'))}:")
|
| 63 |
+
print(f" Current price: ${crypto.get('current_price', 'N/A')}")
|
| 64 |
+
print(f" Market cap: ${crypto.get('market_cap', 'N/A')}")
|
| 65 |
+
print(f" 24h change: {crypto.get('day_change_percent', 'N/A'):.2f}%")
|
| 66 |
+
print(f" 7d change: {crypto.get('week_change_percent', 'N/A'):.2f}%")
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"β Error testing Yahoo Crypto Market Tool: {str(e)}")
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
main()
|