1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
import os
import time
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dotenv import load_dotenv
from alpaca.data.historical import NewsClient, StockHistoricalDataClient
from alpaca.data.requests import NewsRequest, StockLatestQuoteRequest
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce
from google.adk.agents import Agent
from vertexai import agent_engines
# Load environment variables
env_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(env_path)
# API Keys
APCA_API_KEY_ID = os.getenv('APCA_API_KEY_ID')
APCA_API_SECRET_KEY = os.getenv('APCA_API_SECRET_KEY')
# Initialize Alpaca Clients
trading_client = TradingClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY, paper=True)
news_client = NewsClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)
stock_client = StockHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)
# Full S&P 500 Tickers List (Retrieved April 2026)
SP500_TICKERS = [
"A", "AAPL", "ABBV", "ABT", "ACGL", "ACN", "ADBE", "ADI", "ADM", "ADP", "ADSK", "AEE", "AEP", "AES", "AFL", "AIG", "AIZ", "AJG", "AKAM", "ALB", "ALGN", "ALL", "ALLE", "AMAT", "AMCR", "AMD", "AME", "AMGN", "AMP", "AMT", "AMZN", "ANET", "AON", "AOS", "APA", "APD", "APH", "APO", "APP", "APTV", "ARE", "ATO", "AVB", "AVGO", "AVY", "AWK", "AXON", "AXP", "AZO", "BA", "BAC", "BALL", "BAX", "BBY", "BDX", "BEN", "BF.B", "BG", "BIIB", "BK", "BKNG", "BKR", "BLDR", "BLK", "BMY", "BR", "BRO", "BRK.B", "BSX", "BWA", "BX", "BXP", "C", "CAG", "CAH", "CARR", "CAT", "CB", "CBOE", "CBRE", "CCI", "CCL", "CDNS", "CDW", "CEG", "CF", "CFG", "CHD", "CHRW", "CHTR", "CI", "CINF", "CL", "CLX", "CMA", "CMCSA", "CME", "CMG", "CMI", "CMS", "CNC", "CNP", "COF", "COO", "COP", "COR", "COST", "CPAY", "CPB", "CPRT", "CPT", "CRL", "CRM", "CRWD", "CSCO", "CSGP", "CSX", "CTAS", "CTSH", "CTVA", "CVS", "CVX", "CZR", "D", "DAL", "DAY", "DD", "DE", "DECK", "DELL", "DFS", "DG", "DGX", "DHI", "DHR", "DIS", "DLR", "DLTR", "DOC", "DOV", "DOW", "DPZ", "DRI", "DTE", "DUK", "DVA", "DVN", "DXCM", "EA", "EBAY", "ECL", "ED", "EFX", "EG", "EIX", "EL", "ELV", "EMN", "EMR", "ENPH", "EOG", "EPAM", "EQT", "EQR", "ERIE", "ES", "ESS", "ETN", "ETR", "EVRG", "EW", "EXC", "EXPD", "EXPE", "EXR", "F", "FANG", "FAST", "FCX", "FDS", "FDX", "FE", "FFIV", "FI", "FICO", "FIS", "FITB", "FMC", "FOX", "FOXA", "FRT", "FSLR", "FTNT", "FTV", "GD", "GDDY", "GE", "GEHC", "GEN", "GEV", "GILD", "GIS", "GL", "GLW", "GM", "GNRC", "GOOG", "GOOGL", "GPC", "GPN", "GRMN", "GS", "GWW", "HAL", "HAS", "HBAN", "HCA", "HD", "HES", "HIG", "HII", "HLT", "HOLX", "HON", "HPE", "HPQ", "HRL", "HSIC", "HST", "HSY", "HUBB", "HUM", "HWM", "IBM", "ICE", "IDXX", "IEX", "IFF", "ILMN", "INCY", "INTC", "INTU", "INVH", "IP", "IPG", "IQV", "IR", "IRM", "ISRG", "IT", "ITW", "IVZ", "J", "JBHT", "JCI", "JKHY", "JNJ", "JPM", "K", "KDP", "KEY", "KEYS", "KHC", "KIM", "KLAC", "KMB", "KMI", "KMX", "KO", "KR", "KVUE", "L", "LDOS", "LEN", "LH", "LHX", "LII", "LIN", "LKQ", "LLY", "LMT", "LNT", "LOW", "LRCX", "LULU", "LUV", "LVS", "LW", "LYB", "LYV", "MA", "MAA", "MAR", "MAS", "MCD", "MCHP", "MCK", "MCO", "MDLZ", "MDT", "MET", "META", "MGM", "MHK", "MKC", "MLM", "MMC", "MMM", "MNST", "MO", "MOH", "MOS", "MPC", "MPWR", "MRK", "MRNA", "MS", "MSCI", "MSFT", "MSI", "MTB", "MTD", "MU", "NDAQ", "NDSN", "NEE", "NEM", "NFLX", "NI", "NKE", "NOC", "NOW", "NRG", "NSC", "NTAP", "NTRS", "NUE", "NVDA", "NVR", "NWS", "NWSA", "NXPI", "O", "ODFL", "OKE", "OMC", "ON", "ORCL", "ORLY", "OTIS", "OXY", "PANW", "PARA", "PAYC", "PAYX", "PCAR", "PCG", "PEAK", "PEG", "PEP", "PFE", "PFG", "PG", "PGR", "PH", "PHM", "PKG", "PLD", "PLTR", "PM", "PNC", "PNR", "PNW", "PODD", "POOL", "PPG", "PPL", "PRU", "PSA", "PSX", "PTC", "PWR", "PYPL", "QCOM", "QRVO", "RCL", "REG", "REGN", "RF", "RHI", "RJF", "RL", "RMD", "ROK", "ROL", "ROP", "ROST", "RSG", "RTX", "RVTY", "SBAC", "SBUX", "SCHW", "SHW", "SJM", "SLB", "SMCI", "SNA", "SNPS", "SO", "SOLV", "SPG", "SPGI", "SRE", "STE", "STLD", "STT", "STX", "STZ", "SWK", "SWKS", "SYF", "SYK", "SYY", "T", "TAP", "TDG", "TDY", "TECH", "TEL", "TER", "TFC", "TFX", "TGT", "TJX", "TKO", "TMUS", "TMO", "TPR", "TRGP", "TRMB", "TROW", "TRV", "TSCO", "TSLA", "TSN", "TT", "TTWO", "TXN", "TXT", "TYL", "UAL", "UBER", "UDR", "UHS", "ULTA", "UNH", "UNP", "UPS", "URI", "USB", "V", "VFC", "VICI", "VLO", "VLTO", "VMC", "VRSK", "VRSN", "VRTX", "VST", "VTR", "VTRS", "VZ", "WAB", "WAT", "WBA", "WBD", "WDC", "WEC", "WELL", "WFC", "WHR", "WM", "WMB", "WMT", "WRB", "WST", "WTW", "WY", "WYNN", "XEL", "XOM", "XYL", "YUM", "ZBH", "ZBRA", "ZION", "ZTS"
]
def get_financial_news() -> str:
"""
Fetches the latest financial news for ALL S&P 500 companies from the last 24 hours.
Splits into two batches to stay within URL length limits.
"""
start_time = datetime.now() - timedelta(days=1)
# Split tickers into two batches of approx 250 each
mid = len(SP500_TICKERS) // 2
batches = [SP500_TICKERS[:mid], SP500_TICKERS[mid:]]
all_formatted_news = []
for batch in batches:
request_params = NewsRequest(
symbols=",".join(batch),
start=start_time,
limit=50
)
time.sleep(1) # Rate-limit delay
try:
news_response = news_client.get_news(request_params)
for article in news_response.data['news']:
# Focus on news stories specific to 1-3 symbols
if 1 <= len(article.symbols) <= 3:
symbols_str = ", ".join(article.symbols)
all_formatted_news.append(
f"Ticker(s): {symbols_str}\n"
f"Headline: {article.headline}\n"
f"Summary: {article.summary}\n"
"---"
)
except Exception as e:
print(f"Error fetching news for batch: {str(e)}")
if not all_formatted_news:
return "No relevant single-stock news found for the S&P 500 in the last 24 hours."
return "\n".join(all_formatted_news)
def get_portfolio_status() -> Dict[str, Any]:
"""Queries Alpaca for account status and open positions."""
time.sleep(1)
account = trading_client.get_account()
time.sleep(1)
positions = trading_client.get_all_positions()
pos_list = []
for p in positions:
pos_list.append({
"symbol": p.symbol,
"qty": float(p.qty),
"market_value": float(p.market_value),
"current_price": float(p.current_price)
})
return {
"available_cash": float(account.cash),
"positions": pos_list
}
def get_latest_price(symbol: str) -> float:
"""Retrieves the most recent ask price for a specific ticker."""
request_params = StockLatestQuoteRequest(symbol_or_symbols=symbol)
time.sleep(1)
quote = stock_client.get_stock_latest_quote(request_params)
return float(quote[symbol].ask_price)
def place_trade_order(symbol: str, side: str, amount_usd: Optional[float] = None, qty: Optional[float] = None) -> str:
"""Places a market order (fractional shares supported for buys)."""
side = side.lower()
if side not in ['buy', 'sell']:
return "Error: Side must be 'buy' or 'sell'."
order_side = OrderSide.BUY if side == 'buy' else OrderSide.SELL
try:
if amount_usd is not None and side == 'buy':
order_request = MarketOrderRequest(
symbol=symbol,
notional=amount_usd,
side=order_side,
time_in_force=TimeInForce.DAY
)
elif qty is not None:
order_request = MarketOrderRequest(
symbol=symbol,
qty=qty,
side=order_side,
time_in_force=TimeInForce.GTC
)
else:
return "Error: Either amount_usd (for buy) or qty (for sell/buy) must be provided."
time.sleep(1)
order = trading_client.submit_order(order_data=order_request)
return f"Success: {side.upper()} order placed for {symbol}. Order ID: {order.id}"
except Exception as e:
return f"Failed to place {side} order for {symbol}: {str(e)}"
# Define the Agent
root_agent = Agent(
name="TradingAgent",
model="gemini-2.5-pro",
instruction="""
You are a sophisticated stock trading AI agent using Gemini 2.5 Pro. Your mission is to execute a daily trading cycle based on news sentiment for the entire S&P 500.
FOLLOW THESE STEPS IN ORDER:
1. NEWS LOOKUP: Call `get_financial_news()` to discover the latest news affecting any of the 500 S&P companies.
2. SENTIMENT ANALYSIS:
- Analyze the news for positive or negative sentiments using Gemini 2.5 Pro and assign a sentiment rating between -1.0 and 1.0.
- CRITICAL: Ignore Stock Analyst Rating changes; market news only.
- CRITICAL: Only consider news stories specific to 1-3 stocks.
- Format your tracking of each analyzed story as: Primary ticker affected, news summary, score.
3. PORTFOLIO QUERY: Call `get_portfolio_status()`.
4. SELL PHASE:
- For every stock held: If sentiment score is NEGATIVE (< 0), sell ALL shares.
- If no negative sentiment, do not sell.
5. BUY PHASE:
- Identify stocks NOT held that have **high confidence positive sentiments**.
- Rank by positivity.
- Create market BUY trades for EXACTLY $1000 of EACH of **up to 5** stocks with highest scores.
- Stop if available cash is less than $1000.
6. FINAL REPORT:
Produce a comprehensive summary:
- SUMMARY OF ORDERS: List all orders placed with ticker, action, score, and news summary.
- MARKET SENTIMENT OVERVIEW: Top 10 Positive and Top 10 Negative news stories (Ticker, Summary, and Score).
Execute the entire cycle once when prompted.
""",
tools=[get_financial_news, get_portfolio_status, get_latest_price, place_trade_order]
)
# Wrap in AdkApp for execution
app = agent_engines.AdkApp(agent=root_agent)
|