kimyechan commited on
Commit
98b6850
·
1 Parent(s): 0b929da

fix:수정

Browse files
Files changed (2) hide show
  1. app.py +87 -24
  2. requirements.txt +2 -1
app.py CHANGED
@@ -4,6 +4,7 @@ import pandas as pd
4
  import torch
5
  import gradio as gr
6
  import yfinance as yf
 
7
 
8
  from chronos import BaseChronosPipeline # from 'chronos-forecasting'
9
 
@@ -19,64 +20,125 @@ def get_pipeline(model_id: str, device: str = "cpu"):
19
  torch_dtype=torch.float32 if device == "cpu" else torch.bfloat16,
20
  )
21
  return _PIPELINE_CACHE[key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- # ---- 주가/크립토 데이터 로딩 (yfinance, 견고화) ----
24
  def load_close_series(ticker: str, start: str, end: str, interval: str = "1d"):
25
  """
26
- BTC-USD 크립토 심볼에서 간헐적으로 timezone/파싱 오류가 나므로
27
- history() 경로를 우선 사용하고, 실패 번 재시도.
28
  """
29
- # 기본값 보정: 너무 최근만 고르면 빈 데이터가 나올 수 있어 일봉은 과거부터 권장
30
- _start = start or "2014-09-17" # BTC-USD 최초 상장일 근처
31
  _end = end or dt.date.today().isoformat()
32
 
33
- tk = yf.Ticker(ticker)
34
  try:
 
35
  df = tk.history(start=_start, end=_end, interval=interval, auto_adjust=True, actions=False)
36
  if df.empty or "Close" not in df:
37
- raise ValueError("empty")
 
 
 
 
 
38
  except Exception:
39
- # fallback: download() 경로 시도
 
 
40
  df = yf.download(ticker, start=_start, end=_end, interval=interval, progress=False, threads=False)
41
- if df.empty or "Close" not in df:
42
- raise ValueError("데이터가 없거나 'Close' 열이 없습니다. 티커/날짜/간격을 확인하세요.")
 
 
43
 
44
- s = df["Close"].dropna().astype(float)
45
- if s.empty:
46
- raise ValueError("다운로드 결과가 비어 있습니다. 기간/간격을 줄이거나 다시 시도하세요.")
47
- return s
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- # ---- 예측 함수 (Gradio가 호출) ----
50
  def run_forecast(ticker, start_date, end_date, horizon, model_id, device, interval):
51
  try:
52
- series = load_close_series(ticker.strip(), start_date, end_date, interval)
 
 
 
 
 
 
 
 
 
53
  except Exception as e:
54
- # Gradio v4에서는 Plot.update가 없음 → None 반환으로 정리
55
  return None, pd.DataFrame(), f"데이터 로딩 오류: {e}"
56
 
57
  pipe = get_pipeline(model_id, device)
58
  H = int(horizon)
59
 
60
- # Chronos 입력: 1D 텐서 (float)
61
  context = torch.tensor(series.values, dtype=torch.float32)
62
-
63
- # 예측: (num_series=1, num_quantiles=3, H) with q=[0.1, 0.5, 0.9]
64
  preds = pipe.predict(context=context, prediction_length=H)[0]
65
  q10, q50, q90 = preds[0], preds[1], preds[2]
66
 
67
- # 표 데이터
68
  df_fcst = pd.DataFrame(
69
  {"q10": q10.numpy(), "q50": q50.numpy(), "q90": q90.numpy()},
70
  index=pd.RangeIndex(1, H + 1, name="step"),
71
  )
72
 
73
- # 미래 x축: interval에 맞는 pandas 주기
74
  import matplotlib.pyplot as plt
75
  freq_map = {"1d": "D", "1h": "H", "30m": "30T", "15m": "15T", "5m": "5T"}
76
  freq = freq_map.get(interval, "D")
77
  future_index = pd.date_range(series.index[-1], periods=H + 1, freq=freq)[1:]
78
 
79
- # 그래프
80
  fig = plt.figure(figsize=(10, 4))
81
  plt.plot(series.index, series.values, label="history")
82
  plt.plot(future_index, q50.numpy(), label="forecast(q50)")
@@ -85,7 +147,8 @@ def run_forecast(ticker, start_date, end_date, horizon, model_id, device, interv
85
  plt.legend()
86
  plt.tight_layout()
87
 
88
- note = "※ 데모 목적입니다. 투자 판단의 책임은 본인에게 있습니다."
 
89
  return fig, df_fcst, note
90
 
91
  # ---- Gradio UI ----
 
4
  import torch
5
  import gradio as gr
6
  import yfinance as yf
7
+ import requests # ← 추가
8
 
9
  from chronos import BaseChronosPipeline # from 'chronos-forecasting'
10
 
 
20
  torch_dtype=torch.float32 if device == "cpu" else torch.bfloat16,
21
  )
22
  return _PIPELINE_CACHE[key]
23
+ # ---- 심볼 매핑: 'BTC-USD' → 'bitcoin' (Coingecko id)
24
+ _CG_MAP = {
25
+ "BTC-USD": "bitcoin",
26
+ "ETH-USD": "ethereum",
27
+ "SOL-USD": "solana",
28
+ "XRP-USD": "ripple",
29
+ "ADA-USD": "cardano",
30
+ }
31
+
32
+ def _fetch_coingecko_daily(ticker: str, start: str, end: str):
33
+ """
34
+ Coingecko: /coins/{id}/market_chart?vs_currency=usd&days=max
35
+ 반환: (date, price) 일별 데이터프레임
36
+ """
37
+ coin_id = _CG_MAP.get(ticker.upper())
38
+ if not coin_id:
39
+ raise ValueError("해당 티커는 Coingecko 매핑이 없습니다. (예: BTC-USD, ETH-USD)")
40
+
41
+ url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/market_chart"
42
+ # days=max 로 전체 일봉 받아온 뒤, 날짜 필터링
43
+ resp = requests.get(url, params={"vs_currency": "usd", "days": "max"}, timeout=30)
44
+ resp.raise_for_status()
45
+ data = resp.json()
46
+ prices = data.get("prices", [])
47
+ if not prices:
48
+ raise ValueError("Coingecko 응답에 prices가 없습니다.")
49
+
50
+ # prices: [[timestamp_ms, price], ...]
51
+ df = pd.DataFrame(prices, columns=["ts", "close"])
52
+ df["ts"] = pd.to_datetime(df["ts"], unit="ms", utc=True).dt.tz_convert(None)
53
+ df = df.set_index("ts").sort_index()
54
+ # 날짜 범위 적용
55
+ s = df["close"].astype(float)
56
+ if start:
57
+ s = s[s.index >= pd.to_datetime(start)]
58
+ if end:
59
+ s = s[s.index <= pd.to_datetime(end)]
60
+ return s
61
 
 
62
  def load_close_series(ticker: str, start: str, end: str, interval: str = "1d"):
63
  """
64
+ 1) yfinance(history download)로 시도
65
+ 2) 실패 Coingecko 일봉으로 대체 (BTC-USD/ETH-USD 등)
66
  """
67
+ ticker = ticker.strip().upper()
68
+ _start = start or "2014-09-17"
69
  _end = end or dt.date.today().isoformat()
70
 
71
+ # ---- 1차: yfinance 시도
72
  try:
73
+ tk = yf.Ticker(ticker)
74
  df = tk.history(start=_start, end=_end, interval=interval, auto_adjust=True, actions=False)
75
  if df.empty or "Close" not in df:
76
+ raise ValueError("empty history")
77
+
78
+ s = df["Close"].dropna().astype(float)
79
+ if s.empty:
80
+ raise ValueError("empty close after dropna")
81
+ return s
82
  except Exception:
83
+ pass
84
+
85
+ try:
86
  df = yf.download(ticker, start=_start, end=_end, interval=interval, progress=False, threads=False)
87
+ if not df.empty and "Close" in df and not df["Close"].dropna().empty:
88
+ return df["Close"].dropna().astype(float)
89
+ except Exception:
90
+ pass
91
 
92
+ # ---- 2차: Coingecko fallback (일봉만)
93
+ try:
94
+ s = _fetch_coingecko_daily(ticker, _start, _end)
95
+ if s.empty:
96
+ raise ValueError("Coingecko 데이터가 비어 있습니다.")
97
+ # interval이 일봉이 아니면 일봉으로 강제 전환 안내 (호출 측에서 메시지로 보여줌)
98
+ if interval != "1d":
99
+ raise RuntimeError("FALLBACK_DAILY_ONLY")
100
+ return s
101
+ except RuntimeError as r:
102
+ if str(r) == "FALLBACK_DAILY_ONLY":
103
+ # 호출부에서 메시지 처리할 수 있게 예외를 다시 던짐
104
+ raise RuntimeError("FALLBACK_DAILY_ONLY")
105
+ raise
106
+ except Exception as e:
107
+ raise ValueError(f"데이터를 가져오지 못했습니다 (yfinance/Coingecko 실패): {e}")
108
 
 
109
  def run_forecast(ticker, start_date, end_date, horizon, model_id, device, interval):
110
  try:
111
+ series = load_close_series(ticker, start_date, end_date, interval)
112
+ fallback_note = ""
113
+ except RuntimeError as r:
114
+ if str(r) == "FALLBACK_DAILY_ONLY":
115
+ # 일봉으로 재시도
116
+ series = load_close_series(ticker, start_date, end_date, "1d")
117
+ interval = "1d"
118
+ fallback_note = "※ Coingecko 대체 소스 사용으로 간격을 '1d(일봉)'로 자동 전환했습니다."
119
+ else:
120
+ return None, pd.DataFrame(), f"데이터 로딩 오류: {r}"
121
  except Exception as e:
 
122
  return None, pd.DataFrame(), f"데이터 로딩 오류: {e}"
123
 
124
  pipe = get_pipeline(model_id, device)
125
  H = int(horizon)
126
 
127
+ import numpy as np
128
  context = torch.tensor(series.values, dtype=torch.float32)
 
 
129
  preds = pipe.predict(context=context, prediction_length=H)[0]
130
  q10, q50, q90 = preds[0], preds[1], preds[2]
131
 
 
132
  df_fcst = pd.DataFrame(
133
  {"q10": q10.numpy(), "q50": q50.numpy(), "q90": q90.numpy()},
134
  index=pd.RangeIndex(1, H + 1, name="step"),
135
  )
136
 
 
137
  import matplotlib.pyplot as plt
138
  freq_map = {"1d": "D", "1h": "H", "30m": "30T", "15m": "15T", "5m": "5T"}
139
  freq = freq_map.get(interval, "D")
140
  future_index = pd.date_range(series.index[-1], periods=H + 1, freq=freq)[1:]
141
 
 
142
  fig = plt.figure(figsize=(10, 4))
143
  plt.plot(series.index, series.values, label="history")
144
  plt.plot(future_index, q50.numpy(), label="forecast(q50)")
 
147
  plt.legend()
148
  plt.tight_layout()
149
 
150
+ base_note = "※ 데모 목적입니다. 투자 판단의 책임은 본인에게 있습니다."
151
+ note = (fallback_note + " " + base_note).strip()
152
  return fig, df_fcst, note
153
 
154
  # ---- Gradio UI ----
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  gradio>=4.44
2
  pandas>=2.2
3
- yfinance==0.2.40
 
4
  matplotlib>=3.8
5
  torch>=2.2 ; platform_system != "Darwin"
6
  chronos-forecasting>=1.0
 
1
  gradio>=4.44
2
  pandas>=2.2
3
+ yfinance==0.2.40 # (우린 실시간 안 써서 이대로 안전)
4
+ requests>=2.31
5
  matplotlib>=3.8
6
  torch>=2.2 ; platform_system != "Darwin"
7
  chronos-forecasting>=1.0