246 lines
8.9 KiB
Python
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()
|