TradingAgents/autonomous/scheduler.py

672 lines
24 KiB
Python

"""
Scheduler
=========
Orchestrates periodic tasks for the autonomous trading system.
Uses APScheduler for task scheduling and asyncio for async execution.
"""
import asyncio
import logging
from typing import Dict, List, Optional, Any
from datetime import datetime, time, timedelta
import os
import sys
# Add parent directory to path for imports
sys.path.append('..')
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from .ibkr_connector import IBKRConnector
from .data_aggregator import DataAggregator
from .signal_processor import SignalProcessor
from .alert_engine import AlertEngine, AlertType, AlertPriority
from .research.ai_research_agent import AIResearchAgent, ResearchQuery, ResearchMode
from .connectors.perplexity_finance import PerplexityFinanceConnector, AnalysisType
logger = logging.getLogger(__name__)
class AutonomousScheduler:
"""
Main scheduler for the autonomous trading system
"""
def __init__(self, config: Optional[Dict] = None):
"""
Initialize the scheduler
Args:
config: Configuration dictionary
"""
self.config = config or {}
self.scheduler = AsyncIOScheduler()
# Initialize components
self.ibkr = IBKRConnector(
host=self.config.get('ibkr_host', '127.0.0.1'),
port=self.config.get('ibkr_port', 7497),
client_id=self.config.get('ibkr_client_id', 1)
)
self.data_agg = DataAggregator(config)
self.signal_processor = SignalProcessor(self.ibkr, self.data_agg, config)
self.alert_engine = AlertEngine(config)
# Initialize AI Research components if API keys are available
self.research_agent = None
self.perplexity = None
if config.get('perplexity_api_key') and config.get('openai_api_key'):
try:
self.perplexity = PerplexityFinanceConnector(
api_key=config.get('perplexity_api_key')
)
self.research_agent = AIResearchAgent(
openai_api_key=config.get('openai_api_key'),
perplexity_connector=self.perplexity
)
logger.info("AI Research Agent initialized")
except Exception as e:
logger.warning(f"Could not initialize AI Research: {e}")
# Track system state
self.is_running = False
self.last_portfolio_check = None
self.last_market_scan = None
self.trading_enabled = self.config.get('trading_enabled', False)
# Your portfolio tickers
self.portfolio_tickers = ["AVGO", "MSFT", "MU", "NVDA", "TSM"]
self.watchlist = self.config.get('watchlist', ["AAPL", "TSLA", "META", "GOOGL"])
async def initialize(self) -> bool:
"""
Initialize connections and verify system readiness
Returns:
True if initialization successful
"""
try:
# Connect to IBKR
logger.info("Connecting to IBKR...")
connected = await self.ibkr.connect()
if not connected:
logger.error("Failed to connect to IBKR. Make sure TWS/Gateway is running.")
await self.alert_engine.send_alert(
title="System Initialization Failed",
message="Could not connect to IBKR. Check TWS/Gateway.",
alert_type=AlertType.SYSTEM,
priority=AlertPriority.CRITICAL
)
return False
# Initial portfolio sync
logger.info("Syncing portfolio...")
await self.ibkr.sync_portfolio()
# Send startup notification
await self.alert_engine.send_alert(
title="🚀 Autonomous Trading System Started",
message=f"System initialized successfully.\nMonitoring {len(self.portfolio_tickers)} positions.",
alert_type=AlertType.SYSTEM,
priority=AlertPriority.INFO
)
self.is_running = True
return True
except Exception as e:
logger.error(f"Initialization error: {e}")
return False
def setup_schedules(self):
"""Configure all scheduled tasks"""
# Portfolio monitoring - every 5 minutes
self.scheduler.add_job(
self.monitor_portfolio,
IntervalTrigger(minutes=5),
id='portfolio_monitor',
name='Portfolio Monitor',
misfire_grace_time=60
)
# Market scanning - every 15 minutes during market hours
self.scheduler.add_job(
self.scan_markets,
IntervalTrigger(minutes=15),
id='market_scanner',
name='Market Scanner',
misfire_grace_time=120
)
# Congressional trades check - every hour
self.scheduler.add_job(
self.check_congressional_trades,
IntervalTrigger(hours=1),
id='congressional_monitor',
name='Congressional Trade Monitor',
misfire_grace_time=300
)
# News monitoring - every 30 minutes
self.scheduler.add_job(
self.monitor_news,
IntervalTrigger(minutes=30),
id='news_monitor',
name='News Monitor',
misfire_grace_time=120
)
# Daily portfolio summary - at 4:30 PM ET (after market close)
self.scheduler.add_job(
self.daily_summary,
CronTrigger(hour=16, minute=30),
id='daily_summary',
name='Daily Summary',
misfire_grace_time=300
)
# Risk check - every 30 minutes during market hours
self.scheduler.add_job(
self.check_risk,
IntervalTrigger(minutes=30),
id='risk_monitor',
name='Risk Monitor',
misfire_grace_time=120
)
# Pre-market check - at 8:30 AM ET
self.scheduler.add_job(
self.premarket_check,
CronTrigger(hour=8, minute=30),
id='premarket_check',
name='Pre-Market Check',
misfire_grace_time=300
)
# AI Research Analysis - every 2 hours during market hours
if self.research_agent:
self.scheduler.add_job(
self.ai_research_analysis,
IntervalTrigger(hours=2),
id='ai_research',
name='AI Research Analysis',
misfire_grace_time=300
)
# Weekly opportunity scan - Sunday evening
self.scheduler.add_job(
self.weekly_opportunity_scan,
CronTrigger(day_of_week=6, hour=18), # Sunday 6 PM
id='weekly_scan',
name='Weekly Opportunity Scan',
misfire_grace_time=600
)
logger.info(f"Scheduled {len(self.scheduler.get_jobs())} jobs")
async def monitor_portfolio(self):
"""Monitor portfolio positions and P&L"""
if not self.is_running:
return
try:
logger.info("Running portfolio monitor...")
# Sync positions
positions = await self.ibkr.sync_portfolio()
if not positions:
logger.warning("No positions found")
return
# Check for significant changes
for ticker, position in positions.items():
# Alert on large losses
if position.percent_change < -5:
await self.alert_engine.send_alert(
title=f"⚠️ Risk Alert: {ticker}",
message=f"{ticker} is down {abs(position.percent_change):.1f}%\n"
f"Current: ${position.current_price:.2f}\n"
f"P&L: ${position.unrealized_pnl:,.2f}",
alert_type=AlertType.RISK_WARNING,
priority=AlertPriority.HIGH,
data={'ticker': ticker, 'loss_percent': position.percent_change}
)
# Alert on large gains
elif position.percent_change > 10:
await self.alert_engine.send_alert(
title=f"📈 Profit Alert: {ticker}",
message=f"{ticker} is up {position.percent_change:.1f}%\n"
f"Consider taking profits.\n"
f"P&L: ${position.unrealized_pnl:,.2f}",
alert_type=AlertType.PORTFOLIO_UPDATE,
priority=AlertPriority.MEDIUM,
data={'ticker': ticker, 'gain_percent': position.percent_change}
)
self.last_portfolio_check = datetime.now()
except Exception as e:
logger.error(f"Portfolio monitor error: {e}")
async def scan_markets(self):
"""Scan markets for trading opportunities"""
if not self.is_running:
return
try:
logger.info("Running market scanner...")
# Get all tickers to scan
all_tickers = list(set(self.portfolio_tickers + self.watchlist))
# Aggregate signals
await self.data_agg.aggregate_signals(all_tickers)
# Process signals
recommendations = []
for ticker in all_tickers:
rec = await self.signal_processor.process_signals(ticker)
if rec and rec.action != 'HOLD' and rec.confidence > 70:
recommendations.append(rec)
# Send top recommendations
recommendations.sort(key=lambda x: x.confidence, reverse=True)
for rec in recommendations[:3]: # Top 3 opportunities
await self.alert_engine.send_trading_signal(rec)
await asyncio.sleep(1) # Avoid flooding
self.last_market_scan = datetime.now()
logger.info(f"Found {len(recommendations)} opportunities")
except Exception as e:
logger.error(f"Market scanner error: {e}")
async def check_congressional_trades(self):
"""Check for new congressional trades"""
if not self.is_running:
return
try:
logger.info("Checking congressional trades...")
# Get recent trades
trades = await self.data_agg.fetch_congressional_trades(
self.portfolio_tickers,
days_back=2
)
# Alert on trades matching portfolio
for trade in trades:
if trade.ticker in self.portfolio_tickers:
emoji = "🟢" if 'purchase' in trade.action else "🔴"
await self.alert_engine.send_alert(
title=f"🏛️ Congressional Trade: {trade.ticker}",
message=f"{emoji} {trade.politician} ({trade.party}-{trade.state}) "
f"{trade.action} {trade.ticker}\n"
f"Amount: {trade.amount_range}\n"
f"Filed: {trade.filing_date.strftime('%Y-%m-%d')}",
alert_type=AlertType.CONGRESSIONAL_TRADE,
priority=AlertPriority.HIGH,
data={
'ticker': trade.ticker,
'politician': trade.politician,
'action': trade.action
}
)
except Exception as e:
logger.error(f"Congressional trade check error: {e}")
async def monitor_news(self):
"""Monitor news for portfolio companies"""
if not self.is_running:
return
try:
logger.info("Monitoring news...")
for ticker in self.portfolio_tickers[:3]: # Limit to avoid rate limits
sentiment = await self.data_agg.fetch_market_sentiment(ticker)
# Alert on strong sentiment
if sentiment['sentiment_score'] > 0.5:
await self.alert_engine.send_alert(
title=f"📰 Positive News: {ticker}",
message=f"Strong positive sentiment detected for {ticker}\n"
f"Sentiment Score: {sentiment['sentiment_score']:.2f}",
alert_type=AlertType.MARKET_MOVE,
priority=AlertPriority.MEDIUM,
data=sentiment
)
await asyncio.sleep(2) # Rate limiting
except Exception as e:
logger.error(f"News monitor error: {e}")
async def check_risk(self):
"""Monitor portfolio risk levels"""
if not self.is_running:
return
try:
logger.info("Checking portfolio risk...")
# Get account info
account_info = await self.ibkr.get_account_info()
if not account_info:
return
portfolio_summary = self.ibkr.get_portfolio_summary()
# Check daily P&L
if account_info.day_pnl < -1000: # Lost more than $1000 today
await self.alert_engine.send_alert(
title="⚠️ Daily Loss Alert",
message=f"Today's P&L: ${account_info.day_pnl:,.2f}\n"
f"Consider reducing risk or stopping for the day.",
alert_type=AlertType.RISK_WARNING,
priority=AlertPriority.HIGH
)
# Check concentration risk
if portfolio_summary['positions']:
max_position = max(portfolio_summary['positions'],
key=lambda x: x['value'])
concentration = max_position['value'] / portfolio_summary['total_value']
if concentration > 0.30: # More than 30% in one position
await self.alert_engine.send_alert(
title=f"⚠️ Concentration Risk: {max_position['ticker']}",
message=f"{max_position['ticker']} is {concentration:.1%} of portfolio\n"
f"Consider rebalancing to reduce risk.",
alert_type=AlertType.RISK_WARNING,
priority=AlertPriority.MEDIUM
)
except Exception as e:
logger.error(f"Risk check error: {e}")
async def premarket_check(self):
"""Pre-market preparation and alerts"""
if not self.is_running:
return
try:
logger.info("Running pre-market check...")
# Check earnings today
earnings = await self.data_agg.fetch_earnings_calendar(
self.portfolio_tickers,
days_ahead=1
)
for event in earnings:
await self.alert_engine.send_alert(
title=f"📊 Earnings Today: {event.ticker}",
message=f"{event.ticker} reports earnings today\n"
f"EPS Estimate: ${event.eps_estimate:.2f}",
alert_type=AlertType.EARNINGS,
priority=AlertPriority.HIGH
)
# Run full market scan
await self.scan_markets()
except Exception as e:
logger.error(f"Pre-market check error: {e}")
async def daily_summary(self):
"""Generate and send daily portfolio summary"""
if not self.is_running:
return
try:
logger.info("Generating daily summary...")
# Get portfolio summary
portfolio_summary = self.ibkr.get_portfolio_summary()
account_info = await self.ibkr.get_account_info()
if portfolio_summary and account_info:
await self.alert_engine.send_portfolio_summary(portfolio_summary)
# Additional daily stats
message = f"""
📊 **Daily Trading Summary**
Account Value: ${account_info.net_liquidation:,.2f}
Today's P&L: ${account_info.day_pnl:+,.2f}
Buying Power: ${account_info.buying_power:,.2f}
Top Opportunities Found: {len(self.signal_processor.recommendations)}
Alerts Sent Today: {len(self.alert_engine.sent_alerts)}
System Status: ✅ All systems operational
"""
await self.alert_engine.send_alert(
title="📈 End of Day Report",
message=message,
alert_type=AlertType.PORTFOLIO_UPDATE,
priority=AlertPriority.INFO
)
except Exception as e:
logger.error(f"Daily summary error: {e}")
async def start(self):
"""Start the autonomous trading system"""
logger.info("Starting Autonomous Trading System...")
# Initialize
if not await self.initialize():
logger.error("Failed to initialize system")
return
# Setup schedules
self.setup_schedules()
# Start scheduler
self.scheduler.start()
logger.info("Scheduler started")
# Run initial scans
await self.monitor_portfolio()
await self.scan_markets()
logger.info("Autonomous Trading System is running")
# Keep running
try:
while self.is_running:
await asyncio.sleep(60) # Check every minute
except KeyboardInterrupt:
logger.info("Shutting down...")
await self.stop()
async def ai_research_analysis(self):
"""Run AI-powered research analysis on portfolio and opportunities"""
if not self.is_running or not self.research_agent:
return
try:
logger.info("Running AI research analysis...")
# Analyze each portfolio position
for ticker in self.portfolio_tickers[:3]: # Limit to avoid rate limits
# Get deep analysis using Perplexity
analysis = await self.perplexity.analyze_stock(
ticker,
AnalysisType.FUNDAMENTAL,
depth="standard"
)
# Alert if significant opportunity or risk
if analysis.rating == "BUY" and analysis.confidence_score > 80:
await self.alert_engine.send_alert(
title=f"🤖 AI Research: Strong Buy - {ticker}",
message=f"Fair Value: ${analysis.fair_value:.2f}\n"
f"Upside: {analysis.upside_potential:.1f}%\n"
f"Confidence: {analysis.confidence_score}%\n\n"
f"{analysis.bull_case[:200]}",
alert_type=AlertType.AI_SIGNAL,
priority=AlertPriority.HIGH,
data={'ticker': ticker, 'analysis': analysis.__dict__}
)
elif analysis.rating == "SELL" and analysis.confidence_score > 80:
await self.alert_engine.send_alert(
title=f"⚠️ AI Research: Consider Selling - {ticker}",
message=f"Fair Value: ${analysis.fair_value:.2f}\n"
f"Downside: {analysis.upside_potential:.1f}%\n\n"
f"Risks: {', '.join(analysis.key_risks[:3])}",
alert_type=AlertType.AI_SIGNAL,
priority=AlertPriority.HIGH,
data={'ticker': ticker, 'analysis': analysis.__dict__}
)
await asyncio.sleep(5) # Rate limiting
# Ask AI for market insights
market_question = "What are the key market risks and opportunities this week?"
market_answer = await self.research_agent.answer_question(market_question)
if market_answer:
await self.alert_engine.send_alert(
title="🧠 AI Market Insights",
message=market_answer[:500],
alert_type=AlertType.MARKET_ANALYSIS,
priority=AlertPriority.MEDIUM
)
except Exception as e:
logger.error(f"AI research analysis error: {e}")
async def weekly_opportunity_scan(self):
"""Weekly scan for new investment opportunities using AI"""
if not self.is_running or not self.research_agent:
return
try:
logger.info("Running weekly opportunity scan...")
# Find undervalued opportunities
opportunities = await self.research_agent.find_opportunities(
investment_amount=10000, # Hypothetical amount for analysis
risk_tolerance="medium",
time_horizon="medium"
)
# Screen for specific opportunities
screening_queries = [
"Find undervalued tech stocks with P/E under 25 and strong growth",
"What dividend stocks are attractive with yields above 3%?",
"Find small-cap growth stocks with revenue growth above 20%"
]
all_recommendations = []
for query in screening_queries:
try:
results = await self.research_agent.screen_stocks(query)
all_recommendations.extend(results[:3]) # Top 3 from each search
await asyncio.sleep(3) # Rate limiting
except Exception as e:
logger.error(f"Screening error for '{query}': {e}")
# Send weekly summary
if all_recommendations:
summary = "📊 **Weekly AI Research Report**\n\n"
summary += "**Top Opportunities Found:**\n"
for rec in all_recommendations[:10]: # Top 10 overall
summary += f"• **{rec.get('ticker', 'N/A')}**: "
summary += f"${rec.get('current_price', 0):.2f}, "
summary += f"Upside: {rec.get('upside_potential', 0):.1f}%\n"
summary += f"\n**Market Outlook:**\n{opportunities.get('market_conditions', {}).get('key_factors', 'N/A')[:300]}"
await self.alert_engine.send_alert(
title="🎯 Weekly Investment Opportunities",
message=summary,
alert_type=AlertType.AI_SIGNAL,
priority=AlertPriority.HIGH,
data={'recommendations': all_recommendations}
)
except Exception as e:
logger.error(f"Weekly opportunity scan error: {e}")
async def stop(self):
"""Stop the autonomous trading system"""
logger.info("Stopping Autonomous Trading System...")
self.is_running = False
# Shutdown scheduler
if self.scheduler.running:
self.scheduler.shutdown(wait=False)
# Disconnect from IBKR
await self.ibkr.disconnect()
# Send shutdown notification
await self.alert_engine.send_alert(
title="🛑 System Shutdown",
message="Autonomous Trading System stopped",
alert_type=AlertType.SYSTEM,
priority=AlertPriority.INFO
)
logger.info("System stopped")
# Main entry point
async def main():
"""Main entry point for the autonomous system"""
# Load configuration
config = {
# IBKR settings
'ibkr_host': os.getenv('IBKR_HOST', '127.0.0.1'),
'ibkr_port': int(os.getenv('IBKR_PORT', 7497)), # 7497 for paper, 7496 for live
'ibkr_client_id': int(os.getenv('IBKR_CLIENT_ID', 1)),
# API keys
'quiver_api_key': os.getenv('QUIVER_API_KEY'),
'alpha_vantage_api_key': os.getenv('ALPHA_VANTAGE_API_KEY'),
'openai_api_key': os.getenv('OPENAI_API_KEY'),
'perplexity_api_key': os.getenv('PERPLEXITY_API_KEY'),
# Notification settings
'discord_webhook_url': os.getenv('DISCORD_WEBHOOK_URL'),
'telegram_bot_token': os.getenv('TELEGRAM_BOT_TOKEN'),
'telegram_chat_id': os.getenv('TELEGRAM_CHAT_ID'),
# Trading settings
'trading_enabled': os.getenv('TRADING_ENABLED', 'false').lower() == 'true',
'watchlist': ['AAPL', 'TSLA', 'META', 'GOOGL', 'AMZN']
}
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Create and start scheduler
scheduler = AutonomousScheduler(config)
await scheduler.start()
if __name__ == "__main__":
asyncio.run(main())