thisispaperdoll commited on
Commit
74fc760
·
verified ·
1 Parent(s): aa2f039

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +12 -0
  2. main.py +337 -0
  3. requirements.txt +7 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
main.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import yfinance as yf
4
+ import pandas as pd
5
+ from datetime import datetime, timedelta
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi_mcp import FastApiMCP
8
+ from typing import Dict, Any, Optional
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+ app = FastAPI(title="Weather & Stock MCP Server")
13
+
14
+ # OpenWeather API 설정
15
+ OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "your_api_key_here")
16
+ OPENWEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather"
17
+
18
+ @app.get("/weather", operation_id="get_weather")
19
+ def get_weather(city: str, country: str = None, units: str = "metric") -> Dict[str, Any]:
20
+ """
21
+ OpenWeather API를 사용해서 지정된 도시의 현재 날씨 정보를 가져옵니다.
22
+
23
+ Args:
24
+ city: 도시 이름 (예: "Seoul", "Tokyo")
25
+ country: 국가 코드 (선택사항, 예: "KR", "JP")
26
+ units: 온도 단위 ("metric"=섭씨, "imperial"=화씨, "kelvin"=켈빈)
27
+
28
+ Returns:
29
+ 날씨 정보 딕셔너리
30
+ """
31
+ try:
32
+ # API 키 확인
33
+ if OPENWEATHER_API_KEY == "your_api_key_here":
34
+ raise HTTPException(
35
+ status_code=400,
36
+ detail="OpenWeather API 키가 설정되지 않았습니다. 환경 변수 OPENWEATHER_API_KEY를 설정하세요."
37
+ )
38
+
39
+ # 도시 이름 구성
40
+ location = city
41
+ if country:
42
+ location = f"{city},{country}"
43
+
44
+ # API 요청 매개변수
45
+ params = {
46
+ "q": location,
47
+ "appid": OPENWEATHER_API_KEY,
48
+ "units": units,
49
+ "lang": "kr" # 한국어 설명
50
+ }
51
+
52
+ # OpenWeather API 호출
53
+ response = requests.get(OPENWEATHER_BASE_URL, params=params, timeout=10)
54
+
55
+ if response.status_code == 404:
56
+ raise HTTPException(status_code=404, detail=f"도시 '{city}'를 찾을 수 없습니다.")
57
+ elif response.status_code == 401:
58
+ raise HTTPException(status_code=401, detail="잘못된 API 키입니다.")
59
+ elif response.status_code != 200:
60
+ raise HTTPException(status_code=response.status_code, detail="날씨 정보를 가져오는데 실패했습니다.")
61
+
62
+ weather_data = response.json()
63
+
64
+ # 응답 데이터 정리
65
+ result = {
66
+ "city": weather_data["name"],
67
+ "country": weather_data["sys"]["country"],
68
+ "temperature": weather_data["main"]["temp"],
69
+ "feels_like": weather_data["main"]["feels_like"],
70
+ "humidity": weather_data["main"]["humidity"],
71
+ "pressure": weather_data["main"]["pressure"],
72
+ "description": weather_data["weather"][0]["description"],
73
+ "wind_speed": weather_data.get("wind", {}).get("speed", 0),
74
+ "visibility": weather_data.get("visibility", 0) / 1000, # km 단위
75
+ "units": units,
76
+ "timestamp": weather_data["dt"]
77
+ }
78
+
79
+ return result
80
+
81
+ except requests.exceptions.RequestException as e:
82
+ raise HTTPException(status_code=500, detail=f"API 요청 실패: {str(e)}")
83
+ except Exception as e:
84
+ raise HTTPException(status_code=500, detail=f"오류 발생: {str(e)}")
85
+
86
+ @app.get("/weather/forecast", operation_id="get_weather_forecast")
87
+ def get_weather_forecast(city: str, country: str = None, units: str = "metric") -> Dict[str, Any]:
88
+ """
89
+ OpenWeather API를 사용해서 5일 날씨 예보를 가져옵니다.
90
+ """
91
+ try:
92
+ if OPENWEATHER_API_KEY == "your_api_key_here":
93
+ raise HTTPException(
94
+ status_code=400,
95
+ detail="OpenWeather API 키가 설정되지 않았습니다."
96
+ )
97
+
98
+ location = city
99
+ if country:
100
+ location = f"{city},{country}"
101
+
102
+ params = {
103
+ "q": location,
104
+ "appid": OPENWEATHER_API_KEY,
105
+ "units": units,
106
+ "lang": "kr"
107
+ }
108
+
109
+ forecast_url = "http://api.openweathermap.org/data/2.5/forecast"
110
+ response = requests.get(forecast_url, params=params, timeout=10)
111
+
112
+ if response.status_code != 200:
113
+ raise HTTPException(status_code=response.status_code, detail="예보 정보를 가져오는데 실패했습니다.")
114
+
115
+ forecast_data = response.json()
116
+
117
+ # 하루에 하나씩만 선택 (정오 12시 기준)
118
+ daily_forecasts = []
119
+ for item in forecast_data["list"][::8]: # 3시간 간격이므로 8개마다 선택
120
+ daily_forecasts.append({
121
+ "date": item["dt_txt"],
122
+ "temperature": item["main"]["temp"],
123
+ "description": item["weather"][0]["description"],
124
+ "humidity": item["main"]["humidity"]
125
+ })
126
+
127
+ return {
128
+ "city": forecast_data["city"]["name"],
129
+ "country": forecast_data["city"]["country"],
130
+ "forecasts": daily_forecasts[:5], # 5일치만
131
+ "units": units
132
+ }
133
+
134
+ except Exception as e:
135
+ raise HTTPException(status_code=500, detail=f"오류 발생: {str(e)}")
136
+
137
+ @app.get("/stock", operation_id="get_stock_data")
138
+ def get_stock_data(
139
+ symbol: str,
140
+ period: str = "1mo",
141
+ interval: str = "1d"
142
+ ) -> Dict[str, Any]:
143
+ """
144
+ yfinance를 사용해서 주식 데이터를 가져옵니다.
145
+
146
+ Args:
147
+ symbol: 주식 심볼 (예: "AAPL", "005930.KS" (삼성전자), "TSLA")
148
+ period: 기간 ("1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max")
149
+ interval: 간격 ("1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo", "3mo")
150
+
151
+ Returns:
152
+ 주식 데이터와 기본 정보
153
+ """
154
+ try:
155
+ # Ticker 객체 생성
156
+ ticker = yf.Ticker(symbol)
157
+
158
+ # 주식 정보 가져오기
159
+ info = ticker.info
160
+
161
+ # 히스토리 데이터 가져오기
162
+ hist = ticker.history(period=period, interval=interval)
163
+
164
+ if hist.empty:
165
+ raise HTTPException(status_code=404, detail=f"심볼 '{symbol}'의 데이터를 찾을 수 없습니다.")
166
+
167
+ # DataFrame을 딕셔너리로 변환
168
+ hist_dict = {}
169
+ for date, row in hist.iterrows():
170
+ date_str = date.strftime('%Y-%m-%d %H:%M:%S') if hasattr(date, 'strftime') else str(date)
171
+ hist_dict[date_str] = {
172
+ "open": float(row['Open']) if pd.notna(row['Open']) else None,
173
+ "high": float(row['High']) if pd.notna(row['High']) else None,
174
+ "low": float(row['Low']) if pd.notna(row['Low']) else None,
175
+ "close": float(row['Close']) if pd.notna(row['Close']) else None,
176
+ "volume": int(row['Volume']) if pd.notna(row['Volume']) else None
177
+ }
178
+
179
+ # 최신 가격 정보
180
+ latest_data = hist.iloc[-1]
181
+ current_price = float(latest_data['Close'])
182
+
183
+ # 가격 변화 계산
184
+ if len(hist) > 1:
185
+ prev_close = float(hist.iloc[-2]['Close'])
186
+ price_change = current_price - prev_close
187
+ price_change_percent = (price_change / prev_close) * 100
188
+ else:
189
+ price_change = 0
190
+ price_change_percent = 0
191
+
192
+ # 기본 정보 추출
193
+ company_info = {
194
+ "symbol": symbol,
195
+ "company_name": info.get("longName", info.get("shortName", symbol)),
196
+ "sector": info.get("sector", "N/A"),
197
+ "industry": info.get("industry", "N/A"),
198
+ "market_cap": info.get("marketCap", 0),
199
+ "currency": info.get("currency", "USD")
200
+ }
201
+
202
+ result = {
203
+ "company_info": company_info,
204
+ "current_price": current_price,
205
+ "price_change": price_change,
206
+ "price_change_percent": round(price_change_percent, 2),
207
+ "period": period,
208
+ "interval": interval,
209
+ "data_points": len(hist),
210
+ "historical_data": hist_dict
211
+ }
212
+
213
+ return result
214
+
215
+ except Exception as e:
216
+ raise HTTPException(status_code=500, detail=f"주식 데이터 조회 실패: {str(e)}")
217
+
218
+ @app.get("/stock/info", operation_id="get_stock_info")
219
+ def get_stock_info(symbol: str) -> Dict[str, Any]:
220
+ """
221
+ 특정 주식의 상세 정보를 가져옵니다.
222
+ """
223
+ try:
224
+ ticker = yf.Ticker(symbol)
225
+ info = ticker.info
226
+
227
+ if not info or "symbol" not in info:
228
+ raise HTTPException(status_code=404, detail=f"심볼 '{symbol}'의 정보를 찾을 수 없습니다.")
229
+
230
+ # 주요 정보 추출
231
+ stock_info = {
232
+ "symbol": symbol,
233
+ "company_name": info.get("longName", info.get("shortName", symbol)),
234
+ "sector": info.get("sector", "N/A"),
235
+ "industry": info.get("industry", "N/A"),
236
+ "market_cap": info.get("marketCap", 0),
237
+ "enterprise_value": info.get("enterpriseValue", 0),
238
+ "trailing_pe": info.get("trailingPE", 0),
239
+ "forward_pe": info.get("forwardPE", 0),
240
+ "peg_ratio": info.get("pegRatio", 0),
241
+ "price_to_book": info.get("priceToBook", 0),
242
+ "debt_to_equity": info.get("debtToEquity", 0),
243
+ "return_on_equity": info.get("returnOnEquity", 0),
244
+ "revenue_growth": info.get("revenueGrowth", 0),
245
+ "earnings_growth": info.get("earningsGrowth", 0),
246
+ "current_price": info.get("currentPrice", info.get("regularMarketPrice", 0)),
247
+ "target_high_price": info.get("targetHighPrice", 0),
248
+ "target_low_price": info.get("targetLowPrice", 0),
249
+ "target_mean_price": info.get("targetMeanPrice", 0),
250
+ "recommendation": info.get("recommendationKey", "N/A"),
251
+ "52_week_high": info.get("fiftyTwoWeekHigh", 0),
252
+ "52_week_low": info.get("fiftyTwoWeekLow", 0),
253
+ "dividend_yield": info.get("dividendYield", 0),
254
+ "ex_dividend_date": info.get("exDividendDate", None),
255
+ "currency": info.get("currency", "USD"),
256
+ "exchange": info.get("exchange", "N/A"),
257
+ "website": info.get("website", "N/A"),
258
+ "business_summary": info.get("longBusinessSummary", "N/A")
259
+ }
260
+
261
+ return stock_info
262
+
263
+ except Exception as e:
264
+ raise HTTPException(status_code=500, detail=f"주식 정보 조회 실패: {str(e)}")
265
+
266
+ @app.get("/stock/multiple", operation_id="get_multiple_stocks")
267
+ def get_multiple_stocks(symbols: str, period: str = "1mo") -> Dict[str, Any]:
268
+ """
269
+ 여러 주식의 데이터를 한 번에 가져옵니다.
270
+
271
+ Args:
272
+ symbols: 쉼표로 구분된 주식 심볼들 (예: "AAPL,GOOGL,MSFT")
273
+ period: 기간
274
+ """
275
+ try:
276
+ symbol_list = [s.strip() for s in symbols.split(",")]
277
+
278
+ if len(symbol_list) > 10:
279
+ raise HTTPException(status_code=400, detail="한 번에 최대 10개의 종목만 조회할 수 있습니다.")
280
+
281
+ results = {}
282
+
283
+ for symbol in symbol_list:
284
+ try:
285
+ ticker = yf.Ticker(symbol)
286
+ hist = ticker.history(period=period)
287
+ info = ticker.info
288
+
289
+ if not hist.empty:
290
+ latest_data = hist.iloc[-1]
291
+ current_price = float(latest_data['Close'])
292
+
293
+ # 가격 변화 계산
294
+ if len(hist) > 1:
295
+ prev_close = float(hist.iloc[-2]['Close'])
296
+ price_change = current_price - prev_close
297
+ price_change_percent = (price_change / prev_close) * 100
298
+ else:
299
+ price_change = 0
300
+ price_change_percent = 0
301
+
302
+ results[symbol] = {
303
+ "company_name": info.get("longName", info.get("shortName", symbol)),
304
+ "current_price": current_price,
305
+ "price_change": price_change,
306
+ "price_change_percent": round(price_change_percent, 2),
307
+ "volume": int(latest_data['Volume']) if pd.notna(latest_data['Volume']) else 0,
308
+ "market_cap": info.get("marketCap", 0),
309
+ "currency": info.get("currency", "USD")
310
+ }
311
+ else:
312
+ results[symbol] = {"error": "데이터를 찾을 수 없습니다."}
313
+
314
+ except Exception as e:
315
+ results[symbol] = {"error": str(e)}
316
+
317
+ return {
318
+ "period": period,
319
+ "stocks": results,
320
+ "requested_symbols": symbol_list,
321
+ "successful_count": len([r for r in results.values() if "error" not in r])
322
+ }
323
+
324
+ except Exception as e:
325
+ raise HTTPException(status_code=500, detail=f"다중 주식 조회 실패: {str(e)}")
326
+
327
+ mcp = FastApiMCP(
328
+ app,
329
+ name="Weather & Stock API MCP"
330
+
331
+ )
332
+
333
+ # /mcp 경로에 MCP 서버를 마운트합니다.
334
+ mcp.mount_http(mount_path="/mcp")
335
+ if __name__ == "__main__":
336
+ import uvicorn
337
+ uvicorn.run(app, host="0.0.0.0", port=8001)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ fastapi-mcp
3
+ uvicorn
4
+ requests
5
+ python-dotenv
6
+ yfinance
7
+ pandas