kapr commited on
Commit
b25337f
·
verified ·
1 Parent(s): b6f2f3c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -51
app.py CHANGED
@@ -4,6 +4,7 @@ import requests
4
  from prophet import Prophet
5
  import logging
6
  import plotly.graph_objs as go
 
7
 
8
  logging.basicConfig(level=logging.INFO)
9
 
@@ -14,6 +15,7 @@ logging.basicConfig(level=logging.INFO)
14
  OKX_TICKERS_ENDPOINT = "https://www.okx.com/api/v5/market/tickers?instType=SPOT"
15
  OKX_CANDLE_ENDPOINT = "https://www.okx.com/api/v5/market/candles"
16
 
 
17
  TIMEFRAME_MAPPING = {
18
  "1m": "1m",
19
  "5m": "5m",
@@ -28,9 +30,13 @@ TIMEFRAME_MAPPING = {
28
  "1w": "1W",
29
  }
30
 
 
 
 
 
31
  def fetch_okx_symbols():
32
  """
33
- Fetch the list of symbols (instId) from OKX Spot tickers.
34
  """
35
  logging.info("Fetching symbols from OKX Spot tickers...")
36
  try:
@@ -55,20 +61,28 @@ def fetch_okx_symbols():
55
  logging.error(f"Error fetching OKX symbols: {e}")
56
  return [f"Error: {str(e)}"]
57
 
58
- def fetch_okx_candles(symbol, timeframe="1H", limit=500):
59
- """
60
- Fetch historical candle data for a symbol from OKX.
61
 
62
- Data columns (9):
63
- [ts, o, h, l, c, vol, volCcy, volCcyQuote, confirm]
 
 
 
 
 
64
  """
65
- logging.info(f"Fetching {limit} candles for {symbol} @ {timeframe} from OKX...")
66
  params = {
67
  "instId": symbol,
68
  "bar": timeframe,
69
  "limit": limit
70
  }
71
-
 
 
 
 
 
 
 
72
  try:
73
  resp = requests.get(OKX_CANDLE_ENDPOINT, params=params, timeout=30)
74
  resp.raise_for_status()
@@ -81,13 +95,9 @@ def fetch_okx_candles(symbol, timeframe="1H", limit=500):
81
 
82
  items = json_data.get("data", [])
83
  if not items:
84
- warning_msg = f"No candle data returned for {symbol}."
85
- logging.warning(warning_msg)
86
- return pd.DataFrame(), warning_msg
87
-
88
- # Reverse to chronological (OKX returns newest first)
89
- items.reverse()
90
 
 
91
  columns = [
92
  "ts", "o", "h", "l", "c", "vol",
93
  "volCcy", "volCcyQuote", "confirm"
@@ -100,45 +110,112 @@ def fetch_okx_candles(symbol, timeframe="1H", limit=500):
100
  "l": "low",
101
  "c": "close"
102
  }, inplace=True)
103
-
104
  df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
105
  numeric_cols = ["open", "high", "low", "close", "vol", "volCcy", "volCcyQuote", "confirm"]
106
  df[numeric_cols] = df[numeric_cols].astype(float)
107
 
108
- logging.info(f"Fetched {len(df)} rows for {symbol}.")
109
  return df, ""
110
-
111
  except Exception as e:
112
- err_msg = f"Error fetching candles for {symbol}: {e}"
113
  logging.error(err_msg)
114
  return pd.DataFrame(), err_msg
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ########################################
117
  # Prophet pipeline
118
  ########################################
119
 
120
  def prepare_data_for_prophet(df):
121
  """
122
- Convert the DataFrame to a Prophet-compatible format.
123
  """
124
  if df.empty:
125
  logging.warning("Empty DataFrame, cannot prepare data for Prophet.")
126
  return pd.DataFrame(columns=["ds", "y"])
127
-
128
  df_prophet = df.rename(columns={"timestamp": "ds", "close": "y"})
129
  return df_prophet[["ds", "y"]]
130
 
131
- def prophet_forecast(df_prophet, periods=10, freq="h"):
 
 
 
 
 
 
 
 
 
 
132
  """
133
- Train a Prophet model and forecast.
134
- Using 'h' or 'd' for freq to avoid future deprecation warnings in pandas.
 
 
135
  """
136
  if df_prophet.empty:
137
- logging.warning("Prophet input is empty, no forecast can be generated.")
138
  return pd.DataFrame(), "No data to forecast."
139
 
140
  try:
141
- model = Prophet()
 
 
 
 
 
 
142
  model.fit(df_prophet)
143
  future = model.make_future_dataframe(periods=periods, freq=freq)
144
  forecast = model.predict(future)
@@ -147,35 +224,53 @@ def prophet_forecast(df_prophet, periods=10, freq="h"):
147
  logging.error(f"Forecast error: {e}")
148
  return pd.DataFrame(), f"Forecast error: {e}"
149
 
150
- def prophet_wrapper(df_prophet, forecast_steps, freq):
 
 
 
 
 
 
 
 
 
 
151
  """
152
- Forecast, then slice out only the new/future rows using .loc.
153
  """
154
  if len(df_prophet) < 10:
155
  return pd.DataFrame(), "Not enough data for forecasting (need >=10 rows)."
156
 
157
- full_forecast, err = prophet_forecast(df_prophet, forecast_steps, freq)
 
 
 
 
 
 
 
 
 
158
  if err:
159
  return pd.DataFrame(), err
160
 
161
- # Only future portion
162
  future_only = full_forecast.loc[len(df_prophet):, ["ds", "yhat", "yhat_lower", "yhat_upper"]]
163
  return future_only, ""
164
 
 
165
  ########################################
166
  # Plot helper
167
  ########################################
168
 
169
  def create_line_plot(forecast_df):
170
  """
171
- Create a Plotly line chart with the future forecast (ds vs yhat).
172
- We'll shade the region between yhat_lower and yhat_upper.
173
  """
174
  if forecast_df.empty:
175
- return go.Figure() # empty figure if no data
176
 
177
  fig = go.Figure()
178
- # Main forecast
179
  fig.add_trace(go.Scatter(
180
  x=forecast_df["ds"],
181
  y=forecast_df["yhat"],
@@ -198,7 +293,7 @@ def create_line_plot(forecast_df):
198
  fig.add_trace(go.Scatter(
199
  x=forecast_df["ds"],
200
  y=forecast_df["yhat_upper"],
201
- fill="tonexty", # fill area between upper & lower
202
  mode="lines",
203
  line=dict(width=0, color="lightblue"),
204
  name="Upper"
@@ -212,36 +307,91 @@ def create_line_plot(forecast_df):
212
  )
213
  return fig
214
 
 
215
  ########################################
216
  # Main Gradio logic
217
  ########################################
218
 
219
- def predict(symbol, timeframe, forecast_steps):
 
 
 
 
 
 
 
 
 
 
220
  """
221
- Orchestrate candle fetch + prophet forecast.
 
 
 
222
  """
 
223
  okx_bar = TIMEFRAME_MAPPING.get(timeframe, "1H")
224
- df_raw, err = fetch_okx_candles(symbol, timeframe=okx_bar, limit=500)
 
 
225
  if err:
226
  return pd.DataFrame(), err
227
 
228
  df_prophet = prepare_data_for_prophet(df_raw)
 
 
229
  freq = "h" if "h" in timeframe.lower() else "d"
230
- future_df, err2 = prophet_wrapper(df_prophet, forecast_steps, freq)
 
 
 
 
 
 
 
 
 
 
231
  if err2:
232
  return pd.DataFrame(), err2
233
 
234
  return future_df, ""
235
 
236
- def display_forecast(symbol, timeframe, forecast_steps):
237
- logging.info(f"User requested: symbol={symbol}, timeframe={timeframe}, steps={forecast_steps}")
238
- forecast_df, error = predict(symbol, timeframe, forecast_steps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  if error:
240
  return None, f"Error: {error}"
241
 
242
  fig = create_line_plot(forecast_df)
243
  return fig, forecast_df
244
 
 
245
  def main():
246
  # Fetch OKX symbols
247
  symbols = fetch_okx_symbols()
@@ -249,12 +399,15 @@ def main():
249
  symbols = ["No symbols available"]
250
 
251
  with gr.Blocks() as demo:
252
- gr.Markdown("# Crypto Price Forecasting with Prophet")
253
  gr.Markdown(
254
- "This app gathers recent candles from OKX's spot market and makes short-term predictions using Prophet. "
255
- "You can pick any available symbol and timeframe, then see a future forecast (no historical lines) in a line chart and table."
 
 
256
  )
257
 
 
258
  symbol_dd = gr.Dropdown(
259
  label="Symbol",
260
  choices=symbols,
@@ -265,37 +418,71 @@ def main():
265
  choices=["1m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "1w"],
266
  value="1h"
267
  )
268
- steps_slider = gr.Slider(
 
 
 
 
 
 
 
269
  label="Forecast Steps",
270
  minimum=1,
271
  maximum=100,
272
  value=10
273
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  forecast_btn = gr.Button("Generate Forecast")
275
 
276
- # First output: the line chart
277
  chart_output = gr.Plot(label="Forecast Chart")
278
-
279
- # Second output: the forecast dataframe
280
  df_output = gr.Dataframe(
281
  label="Forecast (Future Only)",
282
  headers=["ds", "yhat", "yhat_lower", "yhat_upper"]
283
  )
284
 
285
- # We return two items from display_forecast: (chart, df)
286
  forecast_btn.click(
287
  fn=display_forecast,
288
- inputs=[symbol_dd, timeframe_dd, steps_slider],
 
 
 
 
 
 
 
 
 
 
289
  outputs=[chart_output, df_output]
290
  )
291
 
 
292
  gr.Markdown(
293
  "For automated trading tools, consider Gunbot as your next [crypto trading bot](https://www.gunbot.com)."
294
  )
295
 
296
-
297
  return demo
298
 
 
299
  if __name__ == "__main__":
300
  app = main()
301
  app.launch()
 
4
  from prophet import Prophet
5
  import logging
6
  import plotly.graph_objs as go
7
+ import math
8
 
9
  logging.basicConfig(level=logging.INFO)
10
 
 
15
  OKX_TICKERS_ENDPOINT = "https://www.okx.com/api/v5/market/tickers?instType=SPOT"
16
  OKX_CANDLE_ENDPOINT = "https://www.okx.com/api/v5/market/candles"
17
 
18
+ # Allowed bar intervals on OKX, maximum 300 records at a time
19
  TIMEFRAME_MAPPING = {
20
  "1m": "1m",
21
  "5m": "5m",
 
30
  "1w": "1W",
31
  }
32
 
33
+ ########################################
34
+ # Functions to fetch data from OKX
35
+ ########################################
36
+
37
  def fetch_okx_symbols():
38
  """
39
+ Fetch spot symbols from OKX.
40
  """
41
  logging.info("Fetching symbols from OKX Spot tickers...")
42
  try:
 
61
  logging.error(f"Error fetching OKX symbols: {e}")
62
  return [f"Error: {str(e)}"]
63
 
 
 
 
64
 
65
+ def fetch_okx_candles_chunk(symbol, timeframe, limit=300, after=None, before=None):
66
+ """
67
+ Fetch up to `limit` candles (max 300) for the given symbol/timeframe.
68
+ Optionally use `after` or `before` to page through older or newer data.
69
+
70
+ OKX returns newest data first. The result here is also newest first.
71
+ We'll reorder or combine them later as needed.
72
  """
 
73
  params = {
74
  "instId": symbol,
75
  "bar": timeframe,
76
  "limit": limit
77
  }
78
+ if after is not None:
79
+ # fetch records older than 'after'
80
+ params["after"] = str(after)
81
+ if before is not None:
82
+ # fetch records newer than 'before'
83
+ params["before"] = str(before)
84
+
85
+ logging.info(f"Fetching chunk: symbol={symbol}, bar={timeframe}, limit={limit}, after={after}, before={before}")
86
  try:
87
  resp = requests.get(OKX_CANDLE_ENDPOINT, params=params, timeout=30)
88
  resp.raise_for_status()
 
95
 
96
  items = json_data.get("data", [])
97
  if not items:
98
+ return pd.DataFrame(), ""
 
 
 
 
 
99
 
100
+ # items are newest first. We'll parse them in that order, then we can reverse later.
101
  columns = [
102
  "ts", "o", "h", "l", "c", "vol",
103
  "volCcy", "volCcyQuote", "confirm"
 
110
  "l": "low",
111
  "c": "close"
112
  }, inplace=True)
 
113
  df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
114
  numeric_cols = ["open", "high", "low", "close", "vol", "volCcy", "volCcyQuote", "confirm"]
115
  df[numeric_cols] = df[numeric_cols].astype(float)
116
 
 
117
  return df, ""
 
118
  except Exception as e:
119
+ err_msg = f"Error fetching candles chunk for {symbol}: {e}"
120
  logging.error(err_msg)
121
  return pd.DataFrame(), err_msg
122
 
123
+
124
+ def fetch_okx_candles(symbol, timeframe="1H", total=2000):
125
+ """
126
+ Fetch ~`total` candles by chaining multiple requests of up to 300 each.
127
+ We'll get the newest data first, then request older data in loops,
128
+ because 'after' param returns records older than the provided ts.
129
+
130
+ Returns df in chronological order (oldest -> newest).
131
+ """
132
+ logging.info(f"Fetching ~{total} candles for {symbol} @ {timeframe} (in multiple chunks).")
133
+
134
+ # We'll do enough calls to get at least `total` data points, or break if no more data.
135
+ calls_needed = math.ceil(total / 300.0)
136
+ all_data = []
137
+ after_ts = None # We'll track the earliest timestamp we see, then pass "after" to go older
138
+
139
+ for _ in range(calls_needed):
140
+ df_chunk, err = fetch_okx_candles_chunk(
141
+ symbol, timeframe, limit=300, after=after_ts
142
+ )
143
+ if err:
144
+ return pd.DataFrame(), err
145
+ if df_chunk.empty:
146
+ # No more data
147
+ break
148
+
149
+ # df_chunk is newest first, so the last row is the earliest in that chunk.
150
+ earliest_ts = df_chunk["timestamp"].iloc[-1]
151
+ # We'll keep chaining to older data by passing after = earliest_ts-1 (in ms).
152
+ # But we need that as a Unix milliseconds integer.
153
+ after_ts = int(earliest_ts.timestamp() * 1000 - 1)
154
+
155
+ # Add this chunk to the big list
156
+ all_data.append(df_chunk)
157
+
158
+ if len(df_chunk) < 300:
159
+ # We didn't get a full chunk, means no more older data available
160
+ break
161
+
162
+ # Concatenate everything
163
+ if not all_data:
164
+ logging.info("No data returned overall.")
165
+ return pd.DataFrame(), "No data returned."
166
+
167
+ df_all = pd.concat(all_data, ignore_index=True)
168
+ # Each chunk is newest first, so the entire df is a bunch of blocks newest->oldest blocks.
169
+ # Let's invert the final large df to chronological
170
+ df_all.sort_values(by="timestamp", inplace=True)
171
+ df_all.reset_index(drop=True, inplace=True)
172
+ logging.info(f"Fetched a total of {len(df_all)} rows for {symbol}.")
173
+ return df_all, ""
174
+
175
+
176
  ########################################
177
  # Prophet pipeline
178
  ########################################
179
 
180
  def prepare_data_for_prophet(df):
181
  """
182
+ Convert DataFrame to Prophet-compatible format: columns ds, y.
183
  """
184
  if df.empty:
185
  logging.warning("Empty DataFrame, cannot prepare data for Prophet.")
186
  return pd.DataFrame(columns=["ds", "y"])
 
187
  df_prophet = df.rename(columns={"timestamp": "ds", "close": "y"})
188
  return df_prophet[["ds", "y"]]
189
 
190
+
191
+ def prophet_forecast(
192
+ df_prophet,
193
+ periods=10,
194
+ freq="h",
195
+ daily_seasonality=False,
196
+ weekly_seasonality=False,
197
+ yearly_seasonality=False,
198
+ seasonality_mode="additive",
199
+ changepoint_prior_scale=0.05,
200
+ ):
201
  """
202
+ Train a Prophet model with various exposed settings:
203
+ - daily/weekly/yearly seasonality toggles
204
+ - seasonality_mode ("additive" or "multiplicative")
205
+ - changepoint_prior_scale (0.01 to ~10, controls overfitting)
206
  """
207
  if df_prophet.empty:
208
+ logging.warning("No data for Prophet.")
209
  return pd.DataFrame(), "No data to forecast."
210
 
211
  try:
212
+ model = Prophet(
213
+ daily_seasonality=daily_seasonality,
214
+ weekly_seasonality=weekly_seasonality,
215
+ yearly_seasonality=yearly_seasonality,
216
+ seasonality_mode=seasonality_mode,
217
+ changepoint_prior_scale=changepoint_prior_scale,
218
+ )
219
  model.fit(df_prophet)
220
  future = model.make_future_dataframe(periods=periods, freq=freq)
221
  forecast = model.predict(future)
 
224
  logging.error(f"Forecast error: {e}")
225
  return pd.DataFrame(), f"Forecast error: {e}"
226
 
227
+
228
+ def prophet_wrapper(
229
+ df_prophet,
230
+ forecast_steps,
231
+ freq,
232
+ daily_seasonality,
233
+ weekly_seasonality,
234
+ yearly_seasonality,
235
+ seasonality_mode,
236
+ changepoint_prior_scale,
237
+ ):
238
  """
239
+ Run the forecast with user-chosen settings, then keep future (new) rows only.
240
  """
241
  if len(df_prophet) < 10:
242
  return pd.DataFrame(), "Not enough data for forecasting (need >=10 rows)."
243
 
244
+ full_forecast, err = prophet_forecast(
245
+ df_prophet,
246
+ periods=forecast_steps,
247
+ freq=freq,
248
+ daily_seasonality=daily_seasonality,
249
+ weekly_seasonality=weekly_seasonality,
250
+ yearly_seasonality=yearly_seasonality,
251
+ seasonality_mode=seasonality_mode,
252
+ changepoint_prior_scale=changepoint_prior_scale,
253
+ )
254
  if err:
255
  return pd.DataFrame(), err
256
 
257
+ # Future portion only: the new rows after the original data
258
  future_only = full_forecast.loc[len(df_prophet):, ["ds", "yhat", "yhat_lower", "yhat_upper"]]
259
  return future_only, ""
260
 
261
+
262
  ########################################
263
  # Plot helper
264
  ########################################
265
 
266
  def create_line_plot(forecast_df):
267
  """
268
+ Make a Plotly line chart from forecast.
 
269
  """
270
  if forecast_df.empty:
271
+ return go.Figure() # empty figure
272
 
273
  fig = go.Figure()
 
274
  fig.add_trace(go.Scatter(
275
  x=forecast_df["ds"],
276
  y=forecast_df["yhat"],
 
293
  fig.add_trace(go.Scatter(
294
  x=forecast_df["ds"],
295
  y=forecast_df["yhat_upper"],
296
+ fill="tonexty",
297
  mode="lines",
298
  line=dict(width=0, color="lightblue"),
299
  name="Upper"
 
307
  )
308
  return fig
309
 
310
+
311
  ########################################
312
  # Main Gradio logic
313
  ########################################
314
 
315
+ def predict(
316
+ symbol,
317
+ timeframe,
318
+ forecast_steps,
319
+ total_candles,
320
+ daily_seasonality,
321
+ weekly_seasonality,
322
+ yearly_seasonality,
323
+ seasonality_mode,
324
+ changepoint_prior_scale,
325
+ ):
326
  """
327
+ 1) Fetch `total_candles` historical data (in multiple parts if needed)
328
+ 2) Convert to Prophet style
329
+ 3) Run forecast with user-specified Prophet settings
330
+ 4) Return future portion
331
  """
332
+ # Convert timeframe to OKX style
333
  okx_bar = TIMEFRAME_MAPPING.get(timeframe, "1H")
334
+
335
+ # This fetch can yield thousands of candles
336
+ df_raw, err = fetch_okx_candles(symbol, timeframe=okx_bar, total=total_candles)
337
  if err:
338
  return pd.DataFrame(), err
339
 
340
  df_prophet = prepare_data_for_prophet(df_raw)
341
+
342
+ # Decide Prophet frequency
343
  freq = "h" if "h" in timeframe.lower() else "d"
344
+
345
+ future_df, err2 = prophet_wrapper(
346
+ df_prophet,
347
+ forecast_steps,
348
+ freq,
349
+ daily_seasonality,
350
+ weekly_seasonality,
351
+ yearly_seasonality,
352
+ seasonality_mode,
353
+ changepoint_prior_scale,
354
+ )
355
  if err2:
356
  return pd.DataFrame(), err2
357
 
358
  return future_df, ""
359
 
360
+
361
+ def display_forecast(
362
+ symbol,
363
+ timeframe,
364
+ forecast_steps,
365
+ total_candles,
366
+ daily_seasonality,
367
+ weekly_seasonality,
368
+ yearly_seasonality,
369
+ seasonality_mode,
370
+ changepoint_prior_scale,
371
+ ):
372
+ logging.info(
373
+ f"User requested: symbol={symbol}, timeframe={timeframe}, steps={forecast_steps}, "
374
+ f"total_candles={total_candles}, daily={daily_seasonality}, weekly={weekly_seasonality}, "
375
+ f"yearly={yearly_seasonality}, mode={seasonality_mode}, cps={changepoint_prior_scale}"
376
+ )
377
+ forecast_df, error = predict(
378
+ symbol,
379
+ timeframe,
380
+ forecast_steps,
381
+ total_candles,
382
+ daily_seasonality,
383
+ weekly_seasonality,
384
+ yearly_seasonality,
385
+ seasonality_mode,
386
+ changepoint_prior_scale,
387
+ )
388
  if error:
389
  return None, f"Error: {error}"
390
 
391
  fig = create_line_plot(forecast_df)
392
  return fig, forecast_df
393
 
394
+
395
  def main():
396
  # Fetch OKX symbols
397
  symbols = fetch_okx_symbols()
 
399
  symbols = ["No symbols available"]
400
 
401
  with gr.Blocks() as demo:
402
+ gr.Markdown("# OKX Price Forecasting with Prophet")
403
  gr.Markdown(
404
+ "This tool can gather thousands of historical data points from OKX's spot market "
405
+ "and make forecasts using Prophet. You can tweak Prophet's advanced settings or "
406
+ "increase the candle fetch size for potentially more accurate predictions.\n\n"
407
+ "Simply pick a symbol, timeframe, how many candles (max ~2000), and forecast steps."
408
  )
409
 
410
+ # Input controls
411
  symbol_dd = gr.Dropdown(
412
  label="Symbol",
413
  choices=symbols,
 
418
  choices=["1m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "1w"],
419
  value="1h"
420
  )
421
+ total_candles_slider = gr.Slider(
422
+ label="Total Candles to Fetch",
423
+ minimum=300,
424
+ maximum=3000,
425
+ value=2000,
426
+ step=100
427
+ )
428
+ forecast_steps_slider = gr.Slider(
429
  label="Forecast Steps",
430
  minimum=1,
431
  maximum=100,
432
  value=10
433
  )
434
+
435
+ # Prophet advanced settings
436
+ daily_box = gr.Checkbox(label="Daily Seasonality", value=False)
437
+ weekly_box = gr.Checkbox(label="Weekly Seasonality", value=False)
438
+ yearly_box = gr.Checkbox(label="Yearly Seasonality", value=False)
439
+ seasonality_mode_dd = gr.Dropdown(
440
+ label="Seasonality Mode",
441
+ choices=["additive", "multiplicative"],
442
+ value="additive"
443
+ )
444
+ changepoint_scale_slider = gr.Slider(
445
+ label="Changepoint Prior Scale (0.01 ~ 1.0)",
446
+ minimum=0.01,
447
+ maximum=1.0,
448
+ step=0.01,
449
+ value=0.05
450
+ )
451
+
452
  forecast_btn = gr.Button("Generate Forecast")
453
 
454
+ # Outputs
455
  chart_output = gr.Plot(label="Forecast Chart")
 
 
456
  df_output = gr.Dataframe(
457
  label="Forecast (Future Only)",
458
  headers=["ds", "yhat", "yhat_lower", "yhat_upper"]
459
  )
460
 
461
+ # Hook everything up
462
  forecast_btn.click(
463
  fn=display_forecast,
464
+ inputs=[
465
+ symbol_dd,
466
+ timeframe_dd,
467
+ forecast_steps_slider,
468
+ total_candles_slider,
469
+ daily_box,
470
+ weekly_box,
471
+ yearly_box,
472
+ seasonality_mode_dd,
473
+ changepoint_scale_slider,
474
+ ],
475
  outputs=[chart_output, df_output]
476
  )
477
 
478
+ # You can choose any text variation you like here
479
  gr.Markdown(
480
  "For automated trading tools, consider Gunbot as your next [crypto trading bot](https://www.gunbot.com)."
481
  )
482
 
 
483
  return demo
484
 
485
+
486
  if __name__ == "__main__":
487
  app = main()
488
  app.launch()