kimyechan commited on
Commit
79eb22e
ยท
1 Parent(s): 7355d65

fix : yfinance -> binance

Browse files
Files changed (1) hide show
  1. app.py +137 -119
app.py CHANGED
@@ -2,10 +2,12 @@ import datetime as dt
2
  import pandas as pd
3
  import torch
4
  import gradio as gr
5
- import yfinance as yf
6
  import requests
 
 
 
7
 
8
- from chronos import BaseChronosPipeline # pip: chronos-forecasting
9
 
10
 
11
  # =============================
@@ -13,6 +15,7 @@ from chronos import BaseChronosPipeline # pip: chronos-forecasting
13
  # =============================
14
  _PIPELINE_CACHE = {}
15
 
 
16
  def get_pipeline(model_id: str, device: str = "cpu"):
17
  key = (model_id, device)
18
  if key not in _PIPELINE_CACHE:
@@ -25,25 +28,30 @@ def get_pipeline(model_id: str, device: str = "cpu"):
25
 
26
 
27
  # =============================
28
- # Binance (๋ฌด์ธ์ฆ) ํด๋ฐฑ
29
  # =============================
30
  _BINANCE_INTERVAL = {"1d": "1d", "1h": "1h", "30m": "30m", "15m": "15m", "5m": "5m"}
31
 
 
32
  def _yf_to_binance_symbol(ticker: str) -> str | None:
33
  """
34
  BTC-USD -> BTCUSDT, ETH-USD -> ETHUSDT ...
35
- ์ฃผ์‹/์›ํ™”/๊ธฐํƒ€ ์‹ฌ๋ณผ์€ None (Binance ํด๋ฐฑํ•˜์ง€ ์•Š์Œ)
36
  """
37
- t = ticker.upper()
38
  if t.endswith("-USD") and len(t) >= 6:
39
  base = t[:-4] # remove "-USD"
40
  return f"{base}USDT"
41
  return None
42
 
43
- def _fetch_binance_klines(ticker: str, interval: str, start: str | None, end: str | None) -> pd.Series:
 
 
 
44
  """
45
  Binance Klines (๋ฌด์ธ์ฆ)
46
  https://api.binance.com/api/v3/klines
 
47
  ๋ฐ˜ํ™˜: pandas.Series(index=datetime, values=float close)
48
  """
49
  if interval not in _BINANCE_INTERVAL:
@@ -51,7 +59,9 @@ def _fetch_binance_klines(ticker: str, interval: str, start: str | None, end: st
51
 
52
  symbol = _yf_to_binance_symbol(ticker)
53
  if not symbol:
54
- raise ValueError("์ด ํ‹ฐ์ปค๋Š” Binance ํด๋ฐฑ ๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค.")
 
 
55
 
56
  base = "https://api.binance.com/api/v3/klines"
57
 
@@ -63,8 +73,13 @@ def _fetch_binance_klines(ticker: str, interval: str, start: str | None, end: st
63
 
64
  rows = []
65
  cur_start = start_ms
 
66
  while True:
67
- params = {"symbol": symbol, "interval": _BINANCE_INTERVAL[interval], "limit": 1000}
 
 
 
 
68
  if cur_start is not None:
69
  params["startTime"] = cur_start
70
  if end_ms is not None:
@@ -78,22 +93,46 @@ def _fetch_binance_klines(ticker: str, interval: str, start: str | None, end: st
78
 
79
  rows.extend(data)
80
 
 
 
 
 
 
 
 
 
 
 
81
  last_close_time = data[-1][6] # closeTime (ms)
82
  next_start = last_close_time + 1
83
  if cur_start is not None and next_start <= cur_start:
84
  break
85
  cur_start = next_start
86
 
 
87
  if len(data) < 1000:
88
  break
89
 
90
  if not rows:
91
- raise ValueError("Binance์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
92
-
93
- df = pd.DataFrame(rows, columns=[
94
- "openTime","open","high","low","close","volume","closeTime",
95
- "quoteAssetVolume","numTrades","takerBuyBase","takerBuyQuote","ignore"
96
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  df["ts"] = pd.to_datetime(df["closeTime"], unit="ms")
98
  s = df.set_index("ts")["close"].astype(float).sort_index()
99
 
@@ -102,26 +141,23 @@ def _fetch_binance_klines(ticker: str, interval: str, start: str | None, end: st
102
  if end:
103
  s = s[s.index <= pd.to_datetime(end)]
104
  if s.empty:
105
- raise ValueError("Binance ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
 
106
  return s
107
 
108
 
109
- # =============================
110
- # yfinance ์ „์šฉ ๊ฒฌ๊ณ  ๋กœ๋” (+Binance ํด๋ฐฑ)
111
- # =============================
112
- def load_close_series(ticker: str, start: str, end: str, interval: str = "1d") -> pd.Series:
113
  """
114
- yfinance๋งŒ์œผ๋กœ ๋จผ์ € ์‹œ๋„:
115
- 1) Ticker().history(start/end)
116
- 2) download(start/end, repair=True)
117
- 3) period ํด๋ฐฑ (interval๋ณ„ ํ›„๋ณด ์ˆœํšŒ)
118
- ๊ทธ๋ž˜๋„ ์‹คํŒจํ•  ๊ฒฝ์šฐ:
119
- 4) Binance (๋ฌด์ธ์ฆ) ํด๋ฐฑ โ€” BTC-USD ๊ฐ™์€ ์•”ํ˜ธํ™”ํ๋งŒ ๋Œ€์ƒ
120
  """
121
  ticker = ticker.strip().upper()
122
 
123
- # ๋‚ ์งœ ๋ณด์ •
124
- _start = start or "2014-09-17" # BTC-USD ํžˆ์Šคํ† ๋ฆฌ ์‹œ์ž‘ ๊ทผ์ฒ˜
125
  _end = end or dt.date.today().isoformat()
126
  try:
127
  sdt = pd.to_datetime(_start)
@@ -130,133 +166,110 @@ def load_close_series(ticker: str, start: str, end: str, interval: str = "1d") -
130
  sdt, edt = edt, sdt
131
  _start, _end = sdt.date().isoformat(), edt.date().isoformat()
132
  except Exception:
133
- pass
134
-
135
- def _extract_close(df: pd.DataFrame | None) -> pd.Series | None:
136
- if df is None or df.empty:
137
- return None
138
- c = df.get("Close")
139
- if c is None:
140
- return None
141
- c = c.dropna().astype(float)
142
- return c if not c.empty else None
143
-
144
- # 1) history(start/end)
145
- try:
146
- tk = yf.Ticker(ticker)
147
- df = tk.history(start=_start, end=_end, interval=interval, auto_adjust=True, actions=False)
148
- s = _extract_close(df)
149
- if s is not None:
150
- return s
151
- except Exception:
152
- pass
153
-
154
- # 2) download(start/end) + repair=True
155
- try:
156
- df = yf.download(
157
- ticker, start=_start, end=_end, interval=interval,
158
- progress=False, threads=False, repair=True
159
- )
160
- s = _extract_close(df)
161
- if s is not None:
162
- return s
163
- except Exception:
164
- pass
165
-
166
- # 3) period ํด๋ฐฑ
167
- if interval == "1d":
168
- period_candidates = ["max", "10y", "5y", "2y", "1y"]
169
- elif interval == "1h":
170
- period_candidates = ["730d", "365d", "60d"]
171
- else: # 30m/15m/5m
172
- period_candidates = ["60d", "30d", "14d"]
173
-
174
- for per in period_candidates:
175
- try:
176
- df = tk.history(period=per, interval=interval, auto_adjust=True, actions=False)
177
- s = _extract_close(df)
178
- if s is not None:
179
- return s
180
- except Exception:
181
- pass
182
- try:
183
- df = yf.download(
184
- ticker, period=per, interval=interval,
185
- progress=False, threads=False, repair=True
186
- )
187
- s = _extract_close(df)
188
- if s is not None:
189
- return s
190
- except Exception:
191
- pass
192
-
193
- # 4) Binance ํด๋ฐฑ (์•”ํ˜ธํ™”ํ๋งŒ)
194
- try:
195
- s = _fetch_binance_klines(ticker, interval, _start, _end)
196
- return s
197
- except Exception:
198
- pass
199
 
200
- raise ValueError(
201
- "๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ„๊ฒฉ(interval)์ด๋‚˜ ๊ธฐ๊ฐ„(start/end ํ˜น์€ period)์„ ์กฐ์ •ํ•ด ๋‹ค์‹œ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”."
202
- )
203
 
204
 
205
  # =============================
206
  # ์˜ˆ์ธก ํ•จ์ˆ˜ (Gradio ํ•ธ๋“ค๋Ÿฌ)
207
  # =============================
208
  def run_forecast(ticker, start_date, end_date, horizon, model_id, device, interval):
 
209
  try:
210
  series = load_close_series(ticker, start_date, end_date, interval)
211
  except Exception as e:
212
- return None, pd.DataFrame(), f"๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์˜ค๋ฅ˜: {e}"
213
 
214
- pipe = get_pipeline(model_id, device)
215
- H = int(horizon)
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- # Chronos ์ž…๋ ฅ
218
  context = torch.tensor(series.values, dtype=torch.float32)
219
 
220
- # ์˜ˆ์ธก: (num_series=1, num_quantiles=3, H) with q=[0.1, 0.5, 0.9]
221
- preds = pipe.predict(context=context, prediction_length=H)[0]
 
 
 
 
222
  q10, q50, q90 = preds[0], preds[1], preds[2]
223
 
224
- # ํ‘œ
225
  df_fcst = pd.DataFrame(
226
  {"q10": q10.numpy(), "q50": q50.numpy(), "q90": q90.numpy()},
227
  index=pd.RangeIndex(1, H + 1, name="step"),
228
  )
229
 
230
- # ๋ฏธ๋ž˜ x์ถ•
231
- import matplotlib.pyplot as plt
232
  freq_map = {"1d": "D", "1h": "H", "30m": "30T", "15m": "15T", "5m": "5T"}
233
  freq = freq_map.get(interval, "D")
234
  future_index = pd.date_range(series.index[-1], periods=H + 1, freq=freq)[1:]
235
 
236
- # ๊ทธ๋ž˜ํ”„
237
  fig = plt.figure(figsize=(10, 4))
238
  plt.plot(series.index, series.values, label="history")
239
  plt.plot(future_index, q50.numpy(), label="forecast(q50)")
240
- plt.fill_between(future_index, q10.numpy(), q90.numpy(), alpha=0.2, label="q10โ€“q90")
241
- plt.title(f"{ticker} forecast by Chronos-Bolt ({interval}, H={H})")
 
 
 
 
 
 
242
  plt.legend()
243
  plt.tight_layout()
244
 
245
- note = "โ€ป ๋ฐ๋ชจ ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. ํˆฌ์ž ํŒ๋‹จ์˜ ์ฑ…์ž„์€ ๋ณธ์ธ์—๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค."
246
  return fig, df_fcst, note
247
 
248
 
249
  # =============================
250
  # Gradio UI
251
  # =============================
252
- with gr.Blocks(title="Chronos Stock/Crypto Forecast") as demo:
253
- gr.Markdown("# Chronos ์ฃผ๊ฐ€ยทํฌ๋ฆฝํ†  ์˜ˆ์ธก ๋ฐ๋ชจ")
 
 
 
 
254
  with gr.Row():
255
- ticker = gr.Textbox(value="BTC-USD", label="ํ‹ฐ์ปค (์˜ˆ: AAPL, MSFT, 005930.KS, BTC-USD)")
256
- horizon = gr.Slider(5, 365, value=90, step=1, label="์˜ˆ์ธก ์Šคํ… H (๊ฐ„๊ฒฉ ๋‹จ์œ„์™€ ๋™์ผ)")
 
 
 
 
 
 
 
 
 
 
257
  with gr.Row():
258
- start = gr.Textbox(value="2014-09-17", label="์‹œ์ž‘์ผ (YYYY-MM-DD)")
259
- end = gr.Textbox(value=dt.date.today().isoformat(), label="์ข…๋ฃŒ์ผ (YYYY-MM-DD)")
 
 
 
 
 
 
 
260
  with gr.Row():
261
  model_id = gr.Dropdown(
262
  choices=[
@@ -266,14 +279,19 @@ with gr.Blocks(title="Chronos Stock/Crypto Forecast") as demo:
266
  "amazon/chronos-bolt-base",
267
  ],
268
  value="amazon/chronos-bolt-small",
269
- label="๋ชจ๋ธ"
 
 
 
 
 
270
  )
271
- device = gr.Dropdown(choices=["cpu"], value="cpu", label="๋””๋ฐ”์ด์Šค")
272
  interval = gr.Dropdown(
273
  choices=["1d", "1h", "30m", "15m", "5m"],
274
  value="1d",
275
- label="๊ฐ„๊ฒฉ"
276
  )
 
277
  btn = gr.Button("์˜ˆ์ธก ์‹คํ–‰")
278
 
279
  plot = gr.Plot(label="History + Forecast")
@@ -283,7 +301,7 @@ with gr.Blocks(title="Chronos Stock/Crypto Forecast") as demo:
283
  btn.click(
284
  fn=run_forecast,
285
  inputs=[ticker, start, end, horizon, model_id, device, interval],
286
- outputs=[plot, table, note]
287
  )
288
 
289
  if __name__ == "__main__":
 
2
  import pandas as pd
3
  import torch
4
  import gradio as gr
 
5
  import requests
6
+ import matplotlib
7
+ matplotlib.use("Agg") # HF Space ๊ฐ™์ด GUI ์—†๋Š” ํ™˜๊ฒฝ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ
8
+ import matplotlib.pyplot as plt
9
 
10
+ from chronos import BaseChronosPipeline # pip install chronos-forecasting
11
 
12
 
13
  # =============================
 
15
  # =============================
16
  _PIPELINE_CACHE = {}
17
 
18
+
19
  def get_pipeline(model_id: str, device: str = "cpu"):
20
  key = (model_id, device)
21
  if key not in _PIPELINE_CACHE:
 
28
 
29
 
30
  # =============================
31
+ # Binance ์ „์šฉ ๋กœ๋”
32
  # =============================
33
  _BINANCE_INTERVAL = {"1d": "1d", "1h": "1h", "30m": "30m", "15m": "15m", "5m": "5m"}
34
 
35
+
36
  def _yf_to_binance_symbol(ticker: str) -> str | None:
37
  """
38
  BTC-USD -> BTCUSDT, ETH-USD -> ETHUSDT ...
39
+ ๊ทธ ์™ธ ํ˜•์‹์€ None (ํ˜„์žฌ๋Š” -USD ์ฝ”์ธ๋งŒ ์ง€์›)
40
  """
41
+ t = ticker.upper().strip()
42
  if t.endswith("-USD") and len(t) >= 6:
43
  base = t[:-4] # remove "-USD"
44
  return f"{base}USDT"
45
  return None
46
 
47
+
48
+ def _fetch_binance_klines(
49
+ ticker: str, interval: str, start: str | None, end: str | None
50
+ ) -> pd.Series:
51
  """
52
  Binance Klines (๋ฌด์ธ์ฆ)
53
  https://api.binance.com/api/v3/klines
54
+
55
  ๋ฐ˜ํ™˜: pandas.Series(index=datetime, values=float close)
56
  """
57
  if interval not in _BINANCE_INTERVAL:
 
59
 
60
  symbol = _yf_to_binance_symbol(ticker)
61
  if not symbol:
62
+ raise ValueError(
63
+ "์ด ํ‹ฐ์ปค๋Š” Binance ์‹ฌ๋ณผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์˜ˆ: BTC-USD, ETH-USD ํ˜•ํƒœ๋งŒ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค."
64
+ )
65
 
66
  base = "https://api.binance.com/api/v3/klines"
67
 
 
73
 
74
  rows = []
75
  cur_start = start_ms
76
+
77
  while True:
78
+ params = {
79
+ "symbol": symbol,
80
+ "interval": _BINANCE_INTERVAL[interval],
81
+ "limit": 1000,
82
+ }
83
  if cur_start is not None:
84
  params["startTime"] = cur_start
85
  if end_ms is not None:
 
93
 
94
  rows.extend(data)
95
 
96
+ # data[i] = [
97
+ # 0 openTime,
98
+ # 1 open,
99
+ # 2 high,
100
+ # 3 low,
101
+ # 4 close,
102
+ # 5 volume,
103
+ # 6 closeTime,
104
+ # ...
105
+ # ]
106
  last_close_time = data[-1][6] # closeTime (ms)
107
  next_start = last_close_time + 1
108
  if cur_start is not None and next_start <= cur_start:
109
  break
110
  cur_start = next_start
111
 
112
+ # ๋” ์ด์ƒ 1000๊ฐœ ์•ˆ ๋‚˜์˜ค๋ฉด ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€๋กœ ํŒ๋‹จ
113
  if len(data) < 1000:
114
  break
115
 
116
  if not rows:
117
+ raise ValueError("Binance์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
118
+
119
+ df = pd.DataFrame(
120
+ rows,
121
+ columns=[
122
+ "openTime",
123
+ "open",
124
+ "high",
125
+ "low",
126
+ "close",
127
+ "volume",
128
+ "closeTime",
129
+ "quoteAssetVolume",
130
+ "numTrades",
131
+ "takerBuyBase",
132
+ "takerBuyQuote",
133
+ "ignore",
134
+ ],
135
+ )
136
  df["ts"] = pd.to_datetime(df["closeTime"], unit="ms")
137
  s = df.set_index("ts")["close"].astype(float).sort_index()
138
 
 
141
  if end:
142
  s = s[s.index <= pd.to_datetime(end)]
143
  if s.empty:
144
+ raise ValueError("Binance ์‹œ๋ฆฌ์ฆˆ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๊ฐ„/๊ฐ„๊ฒฉ์„ ๋‹ค์‹œ ์„ค์ •ํ•ด ์ฃผ์„ธ์š”.")
145
+
146
  return s
147
 
148
 
149
+ def load_close_series(
150
+ ticker: str, start: str | None, end: str | None, interval: str = "1d"
151
+ ) -> pd.Series:
 
152
  """
153
+ Binance ์ „์šฉ ์ข…๊ฐ€ ์‹œ๋ฆฌ์ฆˆ ๋กœ๋”.
154
+ ์ž…๋ ฅ: ํ‹ฐ์ปค (์˜ˆ: BTC-USD, ETH-USD), ์‹œ์ž‘์ผ, ์ข…๋ฃŒ์ผ, ๊ฐ„๊ฒฉ(1d/1h/30m/15m/5m)
155
+ ๋ฐ˜ํ™˜: pandas.Series (index=datetime, values=float close)
 
 
 
156
  """
157
  ticker = ticker.strip().upper()
158
 
159
+ # ๊ธฐ๋ณธ ๊ธฐ๊ฐ„ ์„ค์ •
160
+ _start = start or "2017-01-01"
161
  _end = end or dt.date.today().isoformat()
162
  try:
163
  sdt = pd.to_datetime(_start)
 
166
  sdt, edt = edt, sdt
167
  _start, _end = sdt.date().isoformat(), edt.date().isoformat()
168
  except Exception:
169
+ # ์ž˜๋ชป๋œ ๋‚ ์งœ ํฌ๋งท์ด ๋“ค์–ด์˜ค๋ฉด ์ผ๋‹จ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
170
+ _start, _end = "2017-01-01", dt.date.today().isoformat()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ # ์˜ค๋กœ์ง€ Binance๋งŒ ์‚ฌ์šฉ
173
+ return _fetch_binance_klines(ticker, interval, _start, _end)
 
174
 
175
 
176
  # =============================
177
  # ์˜ˆ์ธก ํ•จ์ˆ˜ (Gradio ํ•ธ๋“ค๋Ÿฌ)
178
  # =============================
179
  def run_forecast(ticker, start_date, end_date, horizon, model_id, device, interval):
180
+ # 1) ์‹œ๊ณ„์—ด ๋กœ๋”ฉ
181
  try:
182
  series = load_close_series(ticker, start_date, end_date, interval)
183
  except Exception as e:
184
+ return None, pd.DataFrame(), f"๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์˜ค๋ฅ˜ (Binance): {e}"
185
 
186
+ # 2) ํŒŒ์ดํ”„๋ผ์ธ ๋กœ๋”ฉ
187
+ try:
188
+ pipe = get_pipeline(model_id, device)
189
+ except Exception as e:
190
+ return None, pd.DataFrame(), f"๋ชจ๋ธ ๋กœ๋”ฉ ์˜ค๋ฅ˜: {e}"
191
+
192
+ # 3) ์˜ˆ์ธก ๊ธธ์ด
193
+ try:
194
+ H = int(horizon)
195
+ if H <= 0:
196
+ raise ValueError("์˜ˆ์ธก ์Šคํ… H๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
197
+ except Exception:
198
+ return None, pd.DataFrame(), "์˜ˆ์ธก ์Šคํ… H๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
199
 
200
+ # 4) Chronos ์ž…๋ ฅ
201
  context = torch.tensor(series.values, dtype=torch.float32)
202
 
203
+ # 5) ์˜ˆ์ธก: (num_series=1, num_quantiles=3, H) with q=[0.1, 0.5, 0.9]
204
+ try:
205
+ preds = pipe.predict(context=context, prediction_length=H)[0]
206
+ except Exception as e:
207
+ return None, pd.DataFrame(), f"์˜ˆ์ธก ์‹คํ–‰ ์˜ค๋ฅ˜: {e}"
208
+
209
  q10, q50, q90 = preds[0], preds[1], preds[2]
210
 
211
+ # 6) ๊ฒฐ๊ณผ ํ‘œ (DataFrame)
212
  df_fcst = pd.DataFrame(
213
  {"q10": q10.numpy(), "q50": q50.numpy(), "q90": q90.numpy()},
214
  index=pd.RangeIndex(1, H + 1, name="step"),
215
  )
216
 
217
+ # 7) ๋ฏธ๋ž˜ x์ถ• ๋งŒ๋“ค๊ธฐ
 
218
  freq_map = {"1d": "D", "1h": "H", "30m": "30T", "15m": "15T", "5m": "5T"}
219
  freq = freq_map.get(interval, "D")
220
  future_index = pd.date_range(series.index[-1], periods=H + 1, freq=freq)[1:]
221
 
222
+ # 8) ๊ทธ๋ž˜ํ”„ ๊ทธ๋ฆฌ๊ธฐ
223
  fig = plt.figure(figsize=(10, 4))
224
  plt.plot(series.index, series.values, label="history")
225
  plt.plot(future_index, q50.numpy(), label="forecast(q50)")
226
+ plt.fill_between(
227
+ future_index,
228
+ q10.numpy(),
229
+ q90.numpy(),
230
+ alpha=0.2,
231
+ label="q10โ€“q90",
232
+ )
233
+ plt.title(f"{ticker} forecast by Chronos-Bolt (Binance, {interval}, H={H})")
234
  plt.legend()
235
  plt.tight_layout()
236
 
237
+ note = "โ€ป ๋ฐ๋ชจ ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. ํˆฌ์ž ํŒ๋‹จ๊ณผ ๊ฒฐ๊ณผ ์ฑ…์ž„์€ ์ „์ ์œผ๋กœ ๋ณธ์ธ์—๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค."
238
  return fig, df_fcst, note
239
 
240
 
241
  # =============================
242
  # Gradio UI
243
  # =============================
244
+ with gr.Blocks(title="Chronos Crypto Forecast (Binance)") as demo:
245
+ gr.Markdown("# Chronos ํฌ๋ฆฝํ†  ์˜ˆ์ธก ๋ฐ๋ชจ (Binance ์ „์šฉ)")
246
+ gr.Markdown(
247
+ "ํ‹ฐ์ปค๋Š” `BTC-USD`, `ETH-USD` ์ฒ˜๋Ÿผ ์ž…๋ ฅํ•˜๋ฉด ๋‚ด๋ถ€์—์„œ `BTCUSDT`, `ETHUSDT`๋กœ ๋ณ€ํ™˜ํ•ด์„œ Binance์—์„œ ๊ฐ€๊ฒฉ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค."
248
+ )
249
+
250
  with gr.Row():
251
+ ticker = gr.Textbox(
252
+ value="BTC-USD",
253
+ label="ํ‹ฐ์ปค (์˜ˆ: BTC-USD, ETH-USD)",
254
+ )
255
+ horizon = gr.Slider(
256
+ 5,
257
+ 365,
258
+ value=90,
259
+ step=1,
260
+ label="์˜ˆ์ธก ์Šคํ… H (๊ฐ„๊ฒฉ ๋‹จ์œ„์™€ ๋™์ผ)",
261
+ )
262
+
263
  with gr.Row():
264
+ start = gr.Textbox(
265
+ value="2017-01-01",
266
+ label="์‹œ์ž‘์ผ (YYYY-MM-DD)",
267
+ )
268
+ end = gr.Textbox(
269
+ value=dt.date.today().isoformat(),
270
+ label="์ข…๋ฃŒ์ผ (YYYY-MM-DD)",
271
+ )
272
+
273
  with gr.Row():
274
  model_id = gr.Dropdown(
275
  choices=[
 
279
  "amazon/chronos-bolt-base",
280
  ],
281
  value="amazon/chronos-bolt-small",
282
+ label="๋ชจ๋ธ",
283
+ )
284
+ device = gr.Dropdown(
285
+ choices=["cpu"], # ํ•„์š”ํ•˜๋ฉด "cuda" ์ถ”๊ฐ€
286
+ value="cpu",
287
+ label="๋””๋ฐ”์ด์Šค",
288
  )
 
289
  interval = gr.Dropdown(
290
  choices=["1d", "1h", "30m", "15m", "5m"],
291
  value="1d",
292
+ label="๊ฐ„๊ฒฉ (Binance interval)",
293
  )
294
+
295
  btn = gr.Button("์˜ˆ์ธก ์‹คํ–‰")
296
 
297
  plot = gr.Plot(label="History + Forecast")
 
301
  btn.click(
302
  fn=run_forecast,
303
  inputs=[ticker, start, end, horizon, model_id, device, interval],
304
+ outputs=[plot, table, note],
305
  )
306
 
307
  if __name__ == "__main__":