# -*- coding: utf-8 -*- """ Strategy Alpaca Paper Trading Executor (Production-Hardened) ======================================================== Connects to Alpaca Paper Trading and executes the active Strategy momentum logic. Can be run manually or scheduled. Usage: python backend/alpaca_executor.py # Show current signals + positions python backend/alpaca_executor.py --execute # Actually place trades """ import os, sys, json, argparse, smtplib, traceback from datetime import datetime, timedelta from email.mime.text import MIMEText from dotenv import load_dotenv import numpy as np import pandas as pd import yfinance as yf from alpaca.trading.client import TradingClient from alpaca.trading.requests import MarketOrderRequest, GetAssetsRequest from alpaca.trading.enums import OrderSide, TimeInForce, AssetClass # Load environment load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) API_KEY = os.getenv("ALPACA_API_KEY") SECRET_KEY = os.getenv("ALPACA_SECRET_KEY") BASE_URL = os.getenv("ALPACA_BASE_URL", "https://paper-api.alpaca.markets") SMTP_EMAIL = os.getenv("SMTP_EMAIL") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") # Strategy Parameters (validated out-of-sample) STRATEGY_PARAMS = { "rebal_days": 60, } # Universe (same as backtesting) UNIVERSE = [ "AAPL","ABBV","ABT","ACN","ADBE","ADI","ADM","ADP","ADSK","AEE","AEP","AES", "AFL","AIG","AIZ","AJG","AKAM","ALB","ALK","ALL","AMAT","AMD","AME","AMGN", "AMP","AMT","AMZN","AON","AOS","APA","APD","APH","ARE","ATO","AVB","AVGO", "AVY","AWK","AXP","AZO","BA","BAC","BAX","BBY","BDX","BEN","BIO","BIIB", "BK","BKNG","BKR","BLK","BMY","BR","BRK-B","BRO","BSX","BWA","BXP","C", "CAG","CAH","CAT","CB","CBOE","CBRE","CCI","CCL","CDNS","CE","CF","CFG", "CHD","CHRW","CHTR","CI","CINF","CL","CLX","CMCSA","CME","CMG","CMI","CMS", "CNC","CNP","COF","COO","COP","COST","CPB","CPRT","CPT","CRL","CRM","CSCO", "CSX","CTAS","CTSH","CVS","CVX","D","DAL","DD","DE","DG","DGX","DHI","DHR", "DIS","DLR","DLTR","DOV","DPZ","DRI","DTE","DUK","DVA","DVN","EA","EBAY", "ECL","ED","EFX","EIX","EL","EMN","EMR","EOG","EQIX","EQR","EQT","ES","ESS", "ETN","ETR","EVRG","EW","EXC","EXPD","EXPE","EXR","F","FAST","FCX","FDS", "FDX","FE","FFIV","FIS","FISV","FITB","FMC","FOX","FOXA","FRT","FTNT","FTV", "GD","GE","GILD","GIS","GL","GLW","GM","GOOG","GOOGL","GPC","GPN","GRMN", "GS","GWW","HAL","HAS","HBAN","HCA","HD","HOLX","HON","HPE","HPQ","HRL", "HSIC","HST","HSY","HUM","IBM","ICE","IDXX","IEX","IFF","ILMN","INCY","INTC", "INTU","IP","IQV","IR","IRM","ISRG","IT","ITW","IVZ","J","JBHT","JCI","JKHY", "JNJ","JPM","KEY","KEYS","KHC","KIM","KLAC","KMB","KMI","KMX","KO","KR","L", "LDOS","LEN","LH","LHX","LIN","LKQ","LLY","LMT","LNC","LNT","LOW","LRCX", "LUV","LVS","LW","LYB","LYV","MA","MAA","MAR","MAS","MCD","MCHP","MCK","MCO", "MDLZ","MDT","MET","META","MGM","MHK","MKC","MKTX","MLM","MMM","MNST","MO", "MOH","MOS","MPC","MPWR","MRK","MS","MSCI","MSFT","MSI","MTB","MTCH","MTD", "MU","NCLH","NDAQ","NDSN","NEE","NEM","NFLX","NI","NKE","NOC","NOW","NRG", "NSC","NTAP","NTRS","NUE","NVDA","NVR","NWL","NWS","NWSA","NXPI","O","ODFL", "OKE","OMC","ON","ORCL","ORLY","OXY","PAYC","PAYX","PCAR","PCG","PEG","PEP", "PFE","PFG","PG","PGR","PH","PHM","PKG","PLD","PM","PNC","PNR","PNW","POOL", "PPG","PPL","PRU","PSA","PSX","PTC","PVH","PWR","PYPL","QCOM","QRVO","RCL", "REG","REGN","RF","RHI","RJF","RL","RMD","ROK","ROL","ROP","ROST","RSG","RTX", "SBAC","SBUX","SCHW","SEE","SHW","SJM","SLB","SNA","SNPS","SO","SPG","SPGI", "SRE","STE","STT","STX","STZ","SWK","SWKS","SYK","SYY","T","TAP","TDG","TDY", "TECH","TEL","TER","TFC","TFX","TGT","TJX","TMO","TMUS","TPR","TRGP","TRMB", "TROW","TRV","TSCO","TSLA","TSN","TT","TTWO","TXN","TXT","TYL","UAL","UDR", "UHS","ULTA","UNH","UNP","UPS","URI","USB","V","VFC","VLO","VMC","VNO","VRSK", "VRSN","VRTX","VTR","VTRS","VZ","WAB","WAT","WDC","WEC","WELL","WFC","WHR", "WM","WMB","WMT","WRB","WST","WTW","WY","WYNN","XEL","XOM","XYL","YUM","ZBH", "ZBRA","ZION","ZTS", "SMCI","DXCM","DELL","PANW","ET","EPD","MPLX","FANG","HWM","CDW","CSGP", "BBWI","ALLE","AMCR","KDP","SYF","LUMN","DXC","GNRC","ETSY","VEEV","WDAY","SHOP", "ABNB","CRWD","DDOG","SNOW","PLTR","COIN","MELI","TEAM","DASH","TTD", "ZS","MNDY","NET","OKTA","BILL","HUBS","DKNG","U","RIVN","LCID", "SOFI","HOOD","NU","GRAB","SE","SPOT","SNAP","PINS","ROKU","RBLX", "UBER","LYFT","HLT","IHG","ELV","CARR","OTIS","DOW","CTVA","CEG","GEV","SOLV", "VLTO","KVUE","GEHC","ARM","DECK","VST","GDDY","AXON","ERIE","APP", "TPL","RVTY","EG","FBIN","PODD","NTRA","TOST","DOC","CPAY","HUBB", "BX","KKR","APO","ARES","CG","MRNA","INVH","VICI","CZR","CTRA","OGN", ] # ═══════════════════════════════════════════════════════════════ # PRODUCTION HARDENING: Alert System # ═══════════════════════════════════════════════════════════════ def send_alert(subject, body): """Send email alert. Fails silently if SMTP creds are not configured.""" if not SMTP_EMAIL or not SMTP_PASSWORD: print(f" [ALERT] (No SMTP configured) {subject}") return try: msg = MIMEText(body) msg['Subject'] = f"[Strategy Trading Bot] {subject}" msg['From'] = SMTP_EMAIL msg['To'] = SMTP_EMAIL with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server: server.login(SMTP_EMAIL, SMTP_PASSWORD) server.send_message(msg) print(f" [ALERT] Email sent: {subject}") except Exception as e: print(f" [ALERT] Email failed: {e}") # ═══════════════════════════════════════════════════════════════ # PRODUCTION HARDENING: Market Hours Check # ═══════════════════════════════════════════════════════════════ def is_market_open(): """Check if the market is open or was recently open. We run at 3:50 PM ET so market should still be open. Falls back to weekday check if API fails.""" try: client = TradingClient(API_KEY, SECRET_KEY, paper=True) clock = client.get_clock() if clock.is_open: return True # If market just closed, still allow on weekdays if datetime.now().weekday() < 5: print(" Market just closed but today is a trading day. Proceeding.") return True return False except Exception as e: print(f" [ERROR] Could not check market hours: {e}") return datetime.now().weekday() < 5 # ═══════════════════════════════════════════════════════════════ # PRODUCTION HARDENING: Data Validation # ═══════════════════════════════════════════════════════════════ def validate_price_data(prices_df, min_history=200): """Validate price data quality before signal generation. Returns (is_valid, list_of_issues).""" issues = [] for ticker in prices_df.columns: series = prices_df[ticker].dropna() if len(series) < min_history: issues.append(f"{ticker}: insufficient history ({len(series)} days, need {min_history})") if len(series) > 1: daily_returns = series.pct_change().dropna() extreme_moves = daily_returns[daily_returns.abs() > 0.50] if len(extreme_moves) > 0: issues.append(f"{ticker}: suspicious 50%+ move on {extreme_moves.index[0].strftime('%Y-%m-%d')}") if (series <= 0).any(): issues.append(f"{ticker}: zero or negative price detected") if issues: print("\n DATA VALIDATION WARNINGS:") for issue in issues: print(f" - {issue}") return len(issues) == 0, issues def get_client(): """Create Alpaca trading client.""" return TradingClient(API_KEY, SECRET_KEY, paper=True) def get_account_info(client): """Fetch and display account info.""" account = client.get_account() print(f"\n{'='*60}") print(f" ALPACA PAPER TRADING ACCOUNT") print(f"{'='*60}") print(f" Account ID: {account.id}") print(f" Status: {account.status}") print(f" Cash: ${float(account.cash):,.2f}") print(f" Portfolio Value: ${float(account.portfolio_value):,.2f}") print(f" Buying Power: ${float(account.buying_power):,.2f}") print(f" Day Trades: {account.daytrade_count}") return account def calculate_strategy_signals(): """Calculate Strategy signals using the unified module.""" from backend.strategy_signals import get_strategy_live_signals signals = get_strategy_live_signals() top_picks = [p["ticker"] for p in signals["picks"]] vol_scalar = signals["vol_scalar"] is_riskoff = signals["regime"] == "RISK-OFF" print(f"\n{'='*60}") print(f" STRATEGY SIGNALS — {datetime.now().strftime('%Y-%m-%d %H:%M')}") print(f"{'='*60}") print(f" SPY: ${signals['spy_price']:.2f}") print(f" 200d SMA: ${signals['sma200']:.2f}") print(f" Regime: {signals['regime']}") print(f" Vol Scalar: {vol_scalar:.2f}x") print(f" Allocation: {vol_scalar*100:.0f}% invested, {(1-vol_scalar)*100:.0f}% cash") print(f"\n TOP PICKS:") for i, p in enumerate(signals['picks']): print(f" {i+1:>2}. {p['ticker']:<6} | Z-Score: {p.get('sector_z_score', 0):>+5.2f} | Price: ${p['price']:>8.2f}") return top_picks, vol_scalar, is_riskoff def get_current_positions(client): """Get current Alpaca positions.""" positions = client.get_all_positions() pos_dict = {} for p in positions: pos_dict[p.symbol] = { "qty": float(p.qty), "market_value": float(p.market_value), "unrealized_pl": float(p.unrealized_pl), "current_price": float(p.current_price), } return pos_dict def execute_rebalance(client, target_picks, vol_scalar): """Execute the Strategy rebalance on Alpaca. Returns (orders_placed, failed_orders) for status tracking.""" account = client.get_account() portfolio_value = float(account.portfolio_value) # Calculate target allocation invested_pct = vol_scalar per_stock_value = (portfolio_value * invested_pct) / len(target_picks) # Get current positions current = get_current_positions(client) current_tickers = set(current.keys()) target_tickers = set(target_picks) # SELLS: positions not in target to_sell = current_tickers - target_tickers # BUYS: targets not in current to_buy = target_tickers - current_tickers # REBALANCE: positions that stay but need size adjustment to_adjust = current_tickers & target_tickers print(f"\n{'='*60}") print(f" REBALANCE PLAN") print(f"{'='*60}") print(f" Portfolio Value: ${portfolio_value:,.2f}") print(f" Target per stock: ${per_stock_value:,.2f} ({invested_pct*100:.0f}% / {len(target_picks)} stocks)") print(f" SELL {len(to_sell)} positions: {', '.join(sorted(to_sell)) if to_sell else 'None'}") print(f" BUY {len(to_buy)} new positions: {', '.join(sorted(to_buy)) if to_buy else 'None'}") print(f" HOLD {len(to_adjust)} positions (may resize)") orders_placed = [] failed_orders = [] # Execute SELLS first (free up cash) for ticker in to_sell: qty = current[ticker]["qty"] if qty > 0: try: order = client.submit_order( MarketOrderRequest( symbol=ticker, qty=qty, side=OrderSide.SELL, time_in_force=TimeInForce.DAY, ) ) orders_placed.append(f"SELL {qty:.0f} {ticker}") print(f" SELL {qty:.0f} {ticker} -- Order {order.id}") except Exception as e: failed_orders.append(f"SELL {ticker}: {e}") print(f" SELL {ticker} FAILED: {e}") # Execute BUYS for ticker in to_buy: try: order = client.submit_order( MarketOrderRequest( symbol=ticker, notional=round(per_stock_value, 2), side=OrderSide.BUY, time_in_force=TimeInForce.DAY, ) ) orders_placed.append(f"BUY ${per_stock_value:.0f} {ticker}") print(f" BUY ${per_stock_value:.0f} of {ticker} -- Order {order.id}") except Exception as e: failed_orders.append(f"BUY {ticker}: {e}") print(f" BUY {ticker} FAILED: {e}") # Adjust existing positions for ticker in to_adjust: current_value = current[ticker]["market_value"] diff = per_stock_value - current_value if abs(diff) > per_stock_value * 0.10: # Only adjust if >10% off target try: if diff > 0: order = client.submit_order( MarketOrderRequest( symbol=ticker, notional=round(abs(diff), 2), side=OrderSide.BUY, time_in_force=TimeInForce.DAY, ) ) orders_placed.append(f"ADD ${diff:.0f} {ticker}") print(f" ADD ${diff:.0f} to {ticker} -- Order {order.id}") else: sell_qty = abs(diff) / current[ticker]["current_price"] if sell_qty >= 0.01: order = client.submit_order( MarketOrderRequest( symbol=ticker, qty=round(sell_qty, 2), side=OrderSide.SELL, time_in_force=TimeInForce.DAY, ) ) orders_placed.append(f"TRIM {sell_qty:.2f} {ticker}") print(f" TRIM {sell_qty:.2f} of {ticker} -- Order {order.id}") except Exception as e: failed_orders.append(f"ADJUST {ticker}: {e}") print(f" ADJUST {ticker} FAILED: {e}") print(f"\n Orders placed: {len(orders_placed)} | Failed: {len(failed_orders)}") # ── Log with Status Field (Production Hardening) ── status = "completed" if len(failed_orders) == 0 else "partial" if len(orders_placed) == 0 and len(failed_orders) > 0: status = "failed" log_path = os.path.join(os.path.dirname(__file__), "alpaca_trade_log.json") log_entry = { "timestamp": datetime.now().isoformat(), "status": status, "picks": target_picks, "vol_scalar": vol_scalar, "portfolio_value": portfolio_value, "per_stock_target": per_stock_value, "orders": orders_placed, "failed_orders": failed_orders, } logs = [] if os.path.exists(log_path): with open(log_path, "r") as f: logs = json.load(f) logs.append(log_entry) with open(log_path, "w") as f: json.dump(logs, f, indent=2) print(f" Trade log saved ({status}) to {log_path}") return orders_placed, failed_orders def main(): parser = argparse.ArgumentParser(description="Strategy Alpaca Paper Trading Executor") parser.add_argument("--execute", action="store_true", help="Actually place trades (default: dry run)") parser.add_argument("--force", action="store_true", help="Force rebalance even if 60 days haven't passed") args = parser.parse_args() # ── Pre-Flight: Market Hours Check ── if args.execute: print("Checking market hours...") if not is_market_open(): msg = f"Market is closed on {datetime.now().strftime('%Y-%m-%d')}. Skipping execution." print(f"\n {msg}") send_alert("Market Closed - Skipping", msg) sys.exit(0) print(" Market is OPEN. Proceeding.") print("Connecting to Alpaca Paper Trading...") client = get_client() # ── 60-Day Rebalance Check (Only resets on "completed" entries) ── log_path = os.path.join(os.path.dirname(__file__), "alpaca_trade_log.json") days_since_last = 999 if os.path.exists(log_path): with open(log_path, "r") as f: try: logs = json.load(f) # Only count "completed" rebalances for the 60-day cooldown completed_logs = [l for l in logs if l.get("status") == "completed"] if completed_logs: last_date_str = completed_logs[-1].get("timestamp", "").split("T")[0] last_date = datetime.strptime(last_date_str, "%Y-%m-%d") days_since_last = (datetime.now() - last_date).days except Exception as e: print(f"Error reading logs: {e}") if not args.force and days_since_last < STRATEGY_PARAMS["rebal_days"]: print(f"\n{'='*60}") print(f" SKIP REBALANCE: Only {days_since_last} days passed since last completed rebalance.") print(f" Strategy requires {STRATEGY_PARAMS['rebal_days']} days to minimize friction.") print(f" Use --force to override this check.") print(f"{'='*60}") # Log skip so GitHub Actions shows daily activity log_path_skip = os.path.join(os.path.dirname(__file__), "alpaca_trade_log.json") skip_logs = [] if os.path.exists(log_path_skip): with open(log_path_skip, "r") as f: try: skip_logs = json.load(f) except: skip_logs = [] skip_logs.append({ "timestamp": datetime.now().isoformat(), "status": "skipped", "reason": f"Only {days_since_last}/{STRATEGY_PARAMS['rebal_days']} days elapsed", }) with open(log_path_skip, "w") as f: json.dump(skip_logs, f, indent=2) return if args.force: print(f"\n[!] FORCE FLAG USED: Bypassing 60-day check ({days_since_last} days elapsed).") # Show account info account = get_account_info(client) # Show current positions positions = get_current_positions(client) if positions: print(f"\n CURRENT POSITIONS ({len(positions)}):") for ticker, info in sorted(positions.items()): print(f" {ticker:<6} | Qty: {info['qty']:>8.2f} | Value: ${info['market_value']:>10,.2f} | P&L: ${info['unrealized_pl']:>+8.2f}") else: print(f"\n CURRENT POSITIONS: None (fresh account)") # ── Calculate Strategy Signals ── try: top_picks, vol_scalar, is_riskoff = calculate_strategy_signals() except ValueError as e: msg = f"DATA VALIDATION FAILURE: {e}" print(f"\n CRITICAL: {msg}") send_alert("CRITICAL: Data Validation Failed", msg) sys.exit(1) except Exception as e: msg = f"Signal calculation failed: {e}\n{traceback.format_exc()}" print(f"\n ERROR: {msg}") send_alert("ERROR: Signal Calculation Failed", msg) sys.exit(1) # Execute or dry run if args.execute: print(f"\n ** EXECUTING LIVE PAPER TRADES **") orders_placed, failed_orders = execute_rebalance(client, top_picks, vol_scalar) # ── Send Alert ── if failed_orders: body = ( f"Rebalance completed with {len(failed_orders)} failures.\n\n" f"Orders placed: {len(orders_placed)}\n" f"Failed orders:\n" + "\n".join(f" - {f}" for f in failed_orders) ) send_alert("WARNING: Partial Rebalance", body) else: body = ( f"Rebalance completed successfully.\n\n" f"Orders placed: {len(orders_placed)}\n" f"Regime: {'RISK-OFF' if is_riskoff else 'RISK-ON'}\n" f"Vol Scalar: {vol_scalar:.2f}\n" f"Top picks: {', '.join(top_picks)}" ) send_alert("SUCCESS: Rebalance Complete", body) else: print(f"\n ** DRY RUN -- No trades placed **") print(f" Run with --execute to place trades on Alpaca Paper.") print(f"\n{'='*60}") print(f" DONE") print(f"{'='*60}") if __name__ == "__main__": main()