TradingAgents/scripts/shadow_run_daily.py

246 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""
Shadow Run - Daily Paper Trading Execution
Runs after market close (4:30 PM ET) to:
1. Download latest market data
2. Run trading workflow for each ticker
3. Log decisions and metrics to SQLite
4. Update monitoring dashboard data
"""
import sys
import os
import time
import sqlite3
import pandas as pd
import yfinance as yf
from datetime import datetime, timedelta
import logging
# Add project root to path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from tradingagents.workflows.integrated_workflow import IntegratedTradingWorkflow
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("logs/shadow_run.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
DB_PATH = "data/shadow_run.db"
def init_db():
"""Initialize SQLite database if it doesn't exist."""
os.makedirs("data", exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Shadow Trades Table
cursor.execute('''
CREATE TABLE IF NOT EXISTS shadow_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
ticker TEXT,
anon_ticker TEXT,
decision TEXT,
quantity INTEGER,
decision_price REAL,
confidence REAL,
fact_check_passed BOOLEAN,
risk_gate_passed BOOLEAN,
rejection_reason TEXT,
regime TEXT,
volatility REAL,
latency_total REAL,
latency_fact_check REAL,
api_cost_est REAL
)
''')
# Daily Metrics Table
cursor.execute('''
CREATE TABLE IF NOT EXISTS daily_metrics (
date TEXT PRIMARY KEY,
total_attempts INTEGER,
rejections INTEGER,
rejection_rate REAL,
regime_steady BOOLEAN,
avg_slippage REAL,
total_api_cost REAL,
max_latency REAL
)
''')
conn.commit()
conn.close()
def get_market_data(ticker: str) -> dict:
"""Download and prepare market data."""
# Download 100 days of data for warm-up
end_date = datetime.now()
start_date = end_date - timedelta(days=150)
try:
df = yf.download(ticker, start=start_date, end=end_date, progress=False, multi_level_index=False)
if len(df) < 60:
logger.warning(f"Insufficient data for {ticker}: {len(df)} rows")
return None
# Calculate ATR (14-day)
high = df['High']
low = df['Low']
close = df['Close']
current_price = float(close.iloc[-1])
tr1 = high - low
tr2 = (high - close.shift()).abs()
tr3 = (low - close.shift()).abs()
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = tr.rolling(14).mean().iloc[-1]
# Prepare data dict (Risk Gate Expects: close, volume, atr)
market_data = {
"price_series": df['Close'],
"price_data": df, # Full DF for regime detector
"current_price": current_price,
"close": current_price, # REQUIRED by Risk Gate
"volume_avg": float(df['Volume'].mean()),
"volume": float(df['Volume'].iloc[-1]), # REQUIRED by Risk Gate
"atr": float(atr) # Likely needed for position sizing
}
return market_data
except Exception as e:
logger.error(f"Error fetching data for {ticker}: {e}")
return None
def run_shadow_trading():
"""Execute daily shadow trading cycle."""
logger.info("Starting Shadow Run execution...")
# Initialize DB
init_db()
# Configuration
config = {
"anonymizer_seed": "shadow_run_v1",
"use_nli_model": True, # Use real NLI model
"max_json_retries": 2,
"fact_check_latency_budget": 2.0,
"portfolio_value": 100000,
"risk_config": {
"max_position_risk": 0.02,
"max_portfolio_heat": 0.10,
"circuit_breaker": 0.15
}
}
# Initialize Workflow
workflow = IntegratedTradingWorkflow(config)
tickers = ["AAPL", "NVDA", "AMZN", "MSFT", "GOOGL", "TSLA", "AMD", "META"]
today_str = datetime.now().strftime("%Y-%m-%d")
total_cost = 0.0
latencies = []
rejections = 0
trade_count = 0
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Mock LLM agents (replace with real API calls for actual production)
# For now, we reuse the mocks from ignition tests, but in a real shadow run
# these would call GPT-4o-mini
from unittest.mock import Mock
llm_agents = {
"market_analyst": lambda p: Mock(content='{"analyst_type": "market", "key_findings": ["Trend is clearly bullish on daily timeframe", "Volume is increasing on up days", "RSI is in bullish zone but not overbought"], "signal": "BUY", "confidence": 0.8, "reasoning": "The technical setup is looking very strong with price action above key moving averages and momentum indicators confirming the trend direction."}'),
"bull_researcher": lambda p: Mock(content='{"researcher_type": "bull", "key_arguments": ["Revenue growth is accelerating quarter over quarter in key segments", "Market share expansion in cloud computing sector is significant"], "signal": "BUY", "confidence": 0.85, "supporting_evidence": ["Q3 Earnings Report showed 20% growth", "Gartner Magic Quadrant leadership"]}'),
"bear_researcher": lambda p: Mock(content='{"researcher_type": "bear", "key_arguments": ["Valuation multiples are currently at historical highs compared to peers", "Macroeconomic headwinds could impact consumer discretionary spending"], "signal": "HOLD", "confidence": 0.4, "supporting_evidence": ["P/E ratio at 45x forward earnings", "Fed rate hike projections"]}'),
"trader": lambda p: {"trader_investment_plan": "Based on the Market Regime being VOLATILE... FINAL TRANSACTION PROPOSAL: **BUY**", "sender": "Trader"},
}
for ticker in tickers:
logger.info(f"Processing {ticker}...")
market_data = get_market_data(ticker)
if not market_data:
continue
# Ground truth for fact checking (in real run, fetch news/earnings)
ground_truth = {
"price": market_data['current_price'],
"trend": "up" if market_data['current_price'] > market_data['price_series'].iloc[-20] else "down"
}
try:
decision, metrics = workflow.execute_trade_decision(
ticker=ticker,
trading_date=today_str,
market_data=market_data,
ground_truth=ground_truth,
llm_agents=llm_agents
)
# Log to DB
est_cost = 0.003 # Estimated API cost per run
total_cost += est_cost
latencies.append(metrics.total_latency)
if decision.action.value == "HOLD" and (not decision.fact_check_passed or not decision.risk_gate_passed):
rejections += 1
# Get regime info (hacky access, normally returned by execute)
regime_val = "UNKNOWN"
# In a real impl, we'd capture this from the workflow return
cursor.execute('''
INSERT INTO shadow_trades
(date, ticker, anon_ticker, decision, quantity, decision_price,
confidence, fact_check_passed, risk_gate_passed, rejection_reason,
regime, latency_total, latency_fact_check, api_cost_est)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
today_str, ticker, workflow.anonymizer.anonymize_ticker(ticker),
decision.action.value, decision.quantity, market_data['current_price'],
decision.confidence, decision.fact_check_passed, decision.risk_gate_passed,
decision.reasoning if "REJECTED" in decision.reasoning else None,
"VOLATILE", # Placeholder, would get from actual detection
metrics.total_latency, metrics.fact_check_time, est_cost
))
trade_count += 1
conn.commit()
except Exception as e:
logger.error(f"Workflow failed for {ticker}: {e}")
# Daily Summary
rejection_rate = rejections / trade_count if trade_count > 0 else 0
max_latency = max(latencies) if latencies else 0
cursor.execute('''
INSERT OR REPLACE INTO daily_metrics
(date, total_attempts, rejections, rejection_rate, regime_steady,
avg_slippage, total_api_cost, max_latency)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
today_str, trade_count, rejections, rejection_rate,
True, 0.0, total_cost, max_latency
))
conn.commit()
conn.close()
logger.info("Shadow Run completed successfully.")
if __name__ == "__main__":
run_shadow_trading()