Asish Karthikeya Gogineni commited on
Commit
06b52d2
·
1 Parent(s): 318edb7

Feat: Replace limited Alpha Vantage API with free unlimited yfinance data

Browse files
Files changed (2) hide show
  1. alphavantage_mcp.py +79 -192
  2. requirements.txt +12 -14
alphavantage_mcp.py CHANGED
@@ -1,52 +1,23 @@
1
- # alphavantage_mcp.py (Corrected for Free Tier)
2
  from fastapi import FastAPI, HTTPException
3
  import uvicorn
4
- import os
5
- from dotenv import load_dotenv
6
- from alpha_vantage.timeseries import TimeSeries
7
  import logging
 
 
8
 
9
- # --- Configuration ---
10
- load_dotenv()
11
-
12
- # --- Logging Setup (MUST be before we use logger) ---
13
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
- logger = logging.getLogger("AlphaVantage_MCP_Server")
15
-
16
- # --- Get API Key ---
17
- ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
18
-
19
- # Fallback: Try to read from Streamlit secrets file (for cloud deployment)
20
- if not ALPHA_VANTAGE_API_KEY:
21
- try:
22
- import toml
23
- secrets_path = os.path.join(os.path.dirname(__file__), ".streamlit", "secrets.toml")
24
- if os.path.exists(secrets_path):
25
- secrets = toml.load(secrets_path)
26
- ALPHA_VANTAGE_API_KEY = secrets.get("ALPHA_VANTAGE_API_KEY")
27
- logger.info("Loaded ALPHA_VANTAGE_API_KEY from .streamlit/secrets.toml")
28
- except Exception as e:
29
- logger.warning(f"Could not load from secrets.toml: {e}")
30
-
31
- if not ALPHA_VANTAGE_API_KEY:
32
- logger.warning("ALPHA_VANTAGE_API_KEY not found in environment. Market data features will fail.")
33
- else:
34
- logger.info(f"ALPHA_VANTAGE_API_KEY found: {ALPHA_VANTAGE_API_KEY[:4]}...")
35
 
36
- # --- FastAPI App & Alpha Vantage Client ---
37
- app = FastAPI(title="Aegis Alpha Vantage MCP Server")
38
- ts = TimeSeries(key=ALPHA_VANTAGE_API_KEY, output_format='json')
39
 
40
  @app.post("/market_data")
41
  async def get_market_data(payload: dict):
42
  """
43
- Fetches market data using the Alpha Vantage API.
44
- Supports both intraday and daily data based on time_range.
45
- Expects a payload like:
46
- {
47
- "symbol": "NVDA",
48
- "time_range": "INTRADAY" | "1D" | "3D" | "1W" | "1M" | "3M" | "1Y"
49
- }
50
  """
51
  symbol = payload.get("symbol")
52
  time_range = payload.get("time_range", "INTRADAY")
@@ -57,170 +28,86 @@ async def get_market_data(payload: dict):
57
 
58
  logger.info(f"Received market data request for symbol: {symbol}, time_range: {time_range}")
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  try:
61
- # Route to appropriate API based on time range
62
- if time_range == "INTRADAY":
63
- # Intraday data (last 4-6 hours, 5-min intervals)
64
- data, meta_data = ts.get_intraday(symbol=symbol, interval="5min", outputsize='compact')
65
- logger.info(f"Successfully retrieved intraday data for {symbol}")
66
- # Detect Alpha Vantage rate-limit / info response (200 OK but no real data)
67
- if isinstance(data, dict) and "Information" in data:
68
- raise Exception(f"Alpha Vantage rate limit: {data['Information']}")
69
- if isinstance(data, dict) and "Note" in data:
70
- raise Exception(f"Alpha Vantage note (rate limit): {data['Note']}")
71
- meta_data["Source"] = "Real API (Alpha Vantage)"
72
- else:
73
- # Daily data for historical ranges
74
- data, meta_data = ts.get_daily(symbol=symbol, outputsize='full')
75
- logger.info(f"Successfully retrieved daily data for {symbol}")
76
- # Detect Alpha Vantage rate-limit / info response
77
- if isinstance(data, dict) and "Information" in data:
78
- raise Exception(f"Alpha Vantage rate limit: {data['Information']}")
79
- if isinstance(data, dict) and "Note" in data:
80
- raise Exception(f"Alpha Vantage note (rate limit): {data['Note']}")
81
- # Filter data based on time range
82
- data = filter_data_by_time_range(data, time_range)
83
- logger.info(f"Filtered to {len(data)} data points for time_range={time_range}")
84
- meta_data["Source"] = "Real API (Alpha Vantage)"
85
 
86
- return {"status": "success", "data": data, "meta_data": meta_data}
 
 
 
87
 
88
- except Exception as e:
89
- # Catch ALL exceptions to ensure fallback works
90
- logger.error(f"Alpha Vantage API error for symbol {symbol}: {e}")
91
- logger.warning(f"Triggering MOCK DATA fallback for {symbol} due to error.")
92
-
93
- import random
94
- import math
95
- from datetime import datetime, timedelta
96
-
97
- # Seed randomness with symbol AND date to ensure it changes daily
98
- # But stays consistent within the same day
99
- today_str = datetime.now().strftime("%Y-%m-%d %H:%M")
100
- seed_value = f"{symbol}_{today_str}"
101
- random.seed(seed_value)
102
-
103
- mock_data = {}
104
- current_time = datetime.now()
105
-
106
- # Generate unique base price
107
- symbol_hash = sum(ord(c) for c in symbol)
108
- base_price = float(symbol_hash % 500) + 50
109
-
110
- # Force distinct start prices for common stocks
111
- if "AAPL" in symbol: base_price = 150.0
112
- if "TSLA" in symbol: base_price = 250.0
113
- if "NVDA" in symbol: base_price = 450.0
114
- if "MSFT" in symbol: base_price = 350.0
115
- if "GOOG" in symbol: base_price = 130.0
116
- if "AMZN" in symbol: base_price = 140.0
117
-
118
- # Add some daily variation to base price
119
- daily_noise = (hash(today_str) % 100) / 10.0 # -5 to +5 variation
120
- base_price += daily_noise
121
 
122
- trend_direction = 1 if symbol_hash % 2 == 0 else -1
123
- volatility = base_price * 0.02
124
- trend_strength = base_price * 0.001
125
- current_price = base_price
126
-
127
- # Determine number of data points based on time range
128
- if time_range == "INTRADAY":
129
- num_points = 100
130
- time_delta = timedelta(minutes=5)
131
- elif time_range == "1D":
132
- # FIX: 1D needs enough intraday points for a real chart (not just 1!)
133
- num_points = 390 # ~6.5 trading hours at 1-min intervals
134
- time_delta = timedelta(minutes=1)
135
- elif time_range == "3D":
136
- # FIX: 3D needs enough points (not just 3!)
137
- num_points = 72 # 3 days × 24 hourly data points
138
- time_delta = timedelta(hours=1)
139
- elif time_range == "1W":
140
- num_points = 168 # 7 days × 24 hourly data points
141
- time_delta = timedelta(hours=1)
142
- elif time_range == "1M":
143
- num_points = 30
144
- time_delta = timedelta(days=1)
145
- elif time_range == "3M":
146
- num_points = 90
147
- time_delta = timedelta(days=1)
148
- elif time_range == "1Y":
149
- num_points = 365
150
- time_delta = timedelta(days=1)
151
- else:
152
- num_points = 100
153
- time_delta = timedelta(minutes=5)
154
-
155
- for i in range(num_points):
156
- noise = random.uniform(-volatility, volatility)
157
- cycle_1 = (base_price * 0.02) * math.sin(i / 8.0)
158
- cycle_2 = (base_price * 0.01) * math.sin(i / 3.0)
159
- change = noise + (trend_direction * trend_strength)
160
- current_price += change
161
- final_price = current_price + cycle_1 + cycle_2
162
- final_price = max(1.0, final_price)
163
-
164
- t = current_time - (time_delta * (num_points - i - 1))
165
-
166
- # Format timestamp based on data type
167
- if time_range in ["INTRADAY", "1D", "3D", "1W"]:
168
- timestamp_str = t.strftime("%Y-%m-%d %H:%M:%S")
169
  else:
170
- timestamp_str = t.strftime("%Y-%m-%d")
171
-
172
- mock_data[timestamp_str] = {
173
- "1. open": str(round(final_price, 2)),
174
- "2. high": str(round(final_price + (volatility * 0.3), 2)),
175
- "3. low": str(round(final_price - (volatility * 0.3), 2)),
176
- "4. close": str(round(final_price + random.uniform(-0.1, 0.1), 2)),
177
- "5. volume": str(int(random.uniform(100000, 5000000)))
178
- }
179
-
180
- return {
181
- "status": "success",
182
- "data": mock_data,
183
- "meta_data": {
184
- "Information": f"Mock Data ({time_range}) - API Limit/Error",
185
- "Source": "Simulated (Fallback)"
186
  }
 
 
 
 
 
187
  }
188
-
189
-
190
- def filter_data_by_time_range(data: dict, time_range: str) -> dict:
191
- """Filter daily data to the specified time range."""
192
- from datetime import datetime, timedelta
193
-
194
- # Map time ranges to days
195
- range_map = {
196
- "1D": 1,
197
- "3D": 3,
198
- "1W": 7,
199
- "1M": 30,
200
- "3M": 90,
201
- "1Y": 365
202
- }
203
-
204
- days = range_map.get(time_range, 30)
205
- cutoff_date = datetime.now() - timedelta(days=days)
206
-
207
- # Filter data
208
- filtered = {}
209
- for timestamp_str, values in data.items():
210
- try:
211
- timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d")
212
- if timestamp >= cutoff_date:
213
- filtered[timestamp_str] = values
214
- except:
215
- # If parsing fails, include the data point
216
- filtered[timestamp_str] = values
217
-
218
- return filtered
219
 
 
 
 
 
 
220
 
221
  @app.get("/")
222
  def read_root():
223
- return {"message": "Aegis Alpha Vantage MCP Server is operational."}
224
 
225
  # --- Main Execution ---
226
  if __name__ == "__main__":
 
1
+ # alphavantage_mcp.py (Rewritten to use yfinance for unlimited free data)
2
  from fastapi import FastAPI, HTTPException
3
  import uvicorn
 
 
 
4
  import logging
5
+ import yfinance as yf
6
+ from datetime import datetime, timedelta
7
 
8
+ # --- Logging Setup ---
 
 
 
9
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
10
+ logger = logging.getLogger("MarketData_MCP_Server")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # --- FastAPI App ---
13
+ app = FastAPI(title="Aegis Market Data MCP Server (yfinance)")
 
14
 
15
  @app.post("/market_data")
16
  async def get_market_data(payload: dict):
17
  """
18
+ Fetches market data using yfinance (free, no rate limits).
19
+ Returns data in the exact same format expected by the orchestrator.
20
+ Supports time_ranges: "INTRADAY", "1D", "3D", "1W", "1M", "3M", "1Y"
 
 
 
 
21
  """
22
  symbol = payload.get("symbol")
23
  time_range = payload.get("time_range", "INTRADAY")
 
28
 
29
  logger.info(f"Received market data request for symbol: {symbol}, time_range: {time_range}")
30
 
31
+ # Map our time_range to yfinance period/interval
32
+ if time_range == "INTRADAY":
33
+ period = "1d"
34
+ interval = "5m"
35
+ elif time_range == "1D":
36
+ period = "1d"
37
+ interval = "1m"
38
+ elif time_range == "3D":
39
+ period = "5d" # yfinance doesn't have 3d, we'll fetch 5d and filter
40
+ interval = "15m"
41
+ elif time_range == "1W":
42
+ period = "5d" # 5 trading days = 1 week
43
+ interval = "15m"
44
+ elif time_range == "1M":
45
+ period = "1mo"
46
+ interval = "1d"
47
+ elif time_range == "3M":
48
+ period = "3mo"
49
+ interval = "1d"
50
+ elif time_range == "1Y":
51
+ period = "1y"
52
+ interval = "1d"
53
+ else:
54
+ period = "1mo"
55
+ interval = "1d"
56
+
57
  try:
58
+ ticker = yf.Ticker(symbol)
59
+ df = ticker.history(period=period, interval=interval)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ if df.empty:
62
+ raise Exception(f"No data found for symbol {symbol}")
63
+
64
+ logger.info(f"Successfully retrieved {len(df)} data points from yfinance for {symbol}")
65
 
66
+ # Format dataframe into the expected nested dictionary format
67
+ formatted_data = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ # If we fetched 5d for a 3D request, calculate the cutoff
70
+ cutoff_date = None
71
+ if time_range == "3D":
72
+ cutoff_date = getattr(df.index[-1], "tz_localize", lambda x: df.index[-1])(None) - timedelta(days=3)
73
+
74
+ for idx, row in df.iterrows():
75
+ # Filter for 3D request
76
+ if cutoff_date:
77
+ # Remove timezone for comparison to avoid offset-naive/aware errors
78
+ naive_idx = getattr(idx, "tz_localize", lambda x: idx)(None)
79
+ if naive_idx < cutoff_date:
80
+ continue
81
+
82
+ # Format timestamp based on whether it's daily or intraday
83
+ if interval in ["1m", "2m", "5m", "15m", "30m", "60m", "1h"]:
84
+ timestamp_str = idx.strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  else:
86
+ timestamp_str = idx.strftime("%Y-%m-%d")
87
+
88
+ formatted_data[timestamp_str] = {
89
+ "1. open": str(round(row["Open"], 2)),
90
+ "2. high": str(round(row["High"], 2)),
91
+ "3. low": str(round(row["Low"], 2)),
92
+ "4. close": str(round(row["Close"], 2)),
93
+ "5. volume": str(int(row["Volume"]))
 
 
 
 
 
 
 
 
94
  }
95
+
96
+ meta_data = {
97
+ "Information": f"Market Data ({time_range})",
98
+ "Symbol": symbol,
99
+ "Source": "Real API (yfinance)"
100
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ return {"status": "success", "data": formatted_data, "meta_data": meta_data}
103
+
104
+ except Exception as e:
105
+ logger.error(f"yfinance error for symbol {symbol}: {e}")
106
+ raise HTTPException(status_code=500, detail=f"Failed to fetch market data: {str(e)}")
107
 
108
  @app.get("/")
109
  def read_root():
110
+ return {"message": "Aegis Market Data MCP Server (yfinance) is operational."}
111
 
112
  # --- Main Execution ---
113
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -1,15 +1,13 @@
1
- streamlit
2
- langchain
3
- langchain-core
4
- langgraph
5
  pydantic<3,>=2
6
- pandas
7
- plotly
8
- python-dotenv
9
- httpx
10
- alpha_vantage
11
- fastapi
12
- uvicorn[standard]
13
- tavily
14
- langchain_ollama
15
- langchain-google-genai
 
1
+ streamlit>=1.31.0
2
+ langchain>=0.1.0
3
+ langchain-core>=0.1.0
4
+ langgraph>=0.0.40
5
  pydantic<3,>=2
6
+ pandas>=2.0.0
7
+ plotly>=5.18.0
8
+ python-dotenv>=1.0.0
9
+ httpx>=0.25.0
10
+ alpha_vantage>=2.3.1
11
+ tavily-python>=0.3.0
12
+ langchain-google-genai>=1.0.0
13
+ yfinance>=0.2.0