GLM / app.py
Kashaf1's picture
Uploaded complete 89 contents
a720b3d
Raw
History Blame Contribute Delete
28.9 kB
"""
Forex & Crypto Prediction Dashboard โ€” Gradio app, entry point for the
Hugging Face Space (see README.md for the Space YAML header / deployment).
Tabs:
1. Chart & Indicators - candlestick chart + the top 5 technical indicators
2. Prediction - ARIMA / Auto-ARIMA / ARIMA-GARCH / Moirai / TimesFM
forecasts, shown together on one chart for
visual comparison
3. Backtest - walk-forward backtest; each model gets its own
SEPARATE accuracy card + per-candle verify log
(numbers are never averaged across models)
4. Live - poll the newest candle, predict the next one, and
verify the previous prediction once it lands
Educational project โ€” nothing here is financial advice.
"""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import gradio as gr
from config import ALL_DEFAULT_SYMBOLS, CUSTOM_TIMEFRAMES, MODEL_NAMES, YF_NATIVE_INTERVALS
from data.historical import get_historical_cached, get_historical_with_features, DataFetchError
from indicators.indicators import add_all_indicators, latest_signals
from models.registry import get_model, fresh_model
from backtest.backtester import (
run_all_models_backtest, run_comparison_backtest,
BASELINE_LABEL, FEATURED_LABEL,
)
from utils.helpers import infer_step, future_index
TIMEFRAME_CHOICES = list(CUSTOM_TIMEFRAMES.keys())
DISCLAIMER = (
"**Educational / research project โ€” not financial advice.** "
"Forex and crypto trading carries substantial risk of loss."
)
FEATURES_HELP = (
"**10-feature input** feeds each model Open/High/Low/Volume plus RSI, MACD, "
"Bollinger Bands, SMA/EMA and Stochastic โ€” computed leak-free (only data "
"available at each prediction's own point in time) โ€” in addition to the "
"Close-price series every model already uses. Off by default; this changes "
"nothing about the original Close-only behavior unless you turn it on. "
"See features/feature_pipeline.py for exactly what's computed and how "
"each model consumes it."
)
# ---------------------------------------------------------------------------
# Tab 1: Chart & Indicators
# ---------------------------------------------------------------------------
def make_candlestick_figure(df: pd.DataFrame, symbol: str, timeframe: str) -> go.Figure:
fig = go.Figure(data=[go.Candlestick(
x=df.index, open=df["Open"], high=df["High"], low=df["Low"], close=df["Close"],
name=symbol,
)])
if "SMA_20" in df:
fig.add_trace(go.Scatter(x=df.index, y=df["SMA_20"], name="SMA 20", line=dict(width=1)))
if "EMA_20" in df:
fig.add_trace(go.Scatter(x=df.index, y=df["EMA_20"], name="EMA 20", line=dict(width=1)))
if "BB_Upper" in df:
fig.add_trace(go.Scatter(x=df.index, y=df["BB_Upper"], name="BB Upper",
line=dict(width=1, dash="dot")))
fig.add_trace(go.Scatter(x=df.index, y=df["BB_Lower"], name="BB Lower",
line=dict(width=1, dash="dot")))
fig.update_layout(title=f"{symbol} โ€” {timeframe}", xaxis_rangeslider_visible=False,
height=480, margin=dict(l=10, r=10, t=40, b=10))
return fig
def fetch_and_chart(symbol, timeframe, lookback_days):
try:
df = get_historical_cached(symbol, timeframe, lookback_days=int(lookback_days), refresh=True)
df_ind = add_all_indicators(df)
fig = make_candlestick_figure(df_ind, symbol, timeframe)
signals = latest_signals(df_ind)
signal_rows = [[name, str(v["value"]), v["signal"]] for name, v in signals.items()]
status = f"Loaded {len(df)} candles for {symbol} ({timeframe})."
return fig, signal_rows, status
except DataFetchError as e:
return None, [], f"Data error: {e}"
except Exception as e:
return None, [], f"Unexpected error: {e}"
# ---------------------------------------------------------------------------
# Tab 2: Prediction
# ---------------------------------------------------------------------------
def run_prediction(symbol, timeframe, selected_models, horizon, lookback_days, use_features):
if not selected_models:
return None, "Pick at least one model."
try:
# get_historical_with_features fetches a bit of extra real history
# in front and trims it back off after computing indicators on it,
# so `df`/`features_df` cover exactly `lookback_days` with no NaN
# from insufficient warm-up (see data/historical.py's docstring โ€”
# this replaced a plain get_historical_cached() + compute_feature_
# frame() pair that could raise on a short lookback).
df, features_df = get_historical_with_features(
symbol, timeframe, lookback_days=int(lookback_days), use_features=use_features, refresh=True,
)
except Exception as e:
return None, f"Data error: {e}"
close = df["Close"]
horizon = int(horizon)
step = infer_step(df.index)
fig = go.Figure()
tail = close.iloc[-150:]
fig.add_trace(go.Scatter(x=tail.index, y=tail.values, name="Actual Close",
line=dict(color="#888888")))
mode_label = f" [{FEATURED_LABEL}]" if use_features else ""
summary_lines = []
for name in selected_models:
try:
model = get_model(name, symbol, timeframe, use_features=use_features)
forecast = model.predict(close, horizon=horizon, features=features_df)
fc_index = future_index(df.index[-1], step, len(forecast))
fig.add_trace(go.Scatter(x=fc_index, y=forecast, name=f"{name}{mode_label} forecast",
mode="lines+markers"))
direction = "UP" if forecast[-1] > close.iloc[-1] else "DOWN"
summary_lines.append(f"**{name}**{mode_label} โ€” next close (approx.) `{forecast[-1]:.5f}` ({direction})")
except Exception as e:
summary_lines.append(f"**{name}**{mode_label} โ€” failed: {e}")
fig.update_layout(title=f"{symbol} โ€” {timeframe} forecast ({horizon} candle(s) ahead)",
height=450, margin=dict(l=10, r=10, t=40, b=10))
return fig, "\n\n".join(summary_lines) + "\n\n" + DISCLAIMER
# ---------------------------------------------------------------------------
# Tab 3: Backtest
# ---------------------------------------------------------------------------
FEATURE_MODE_BASELINE = "Close-only (baseline)"
FEATURE_MODE_FEATURED = "+10-feature input"
FEATURE_MODE_COMPARE = "Compare both"
def _summary_row(summary: dict, mode_label: str) -> list:
return [
mode_label, summary["model"], summary["total_predictions"], summary["correct"],
summary["incorrect"], f"{summary['accuracy_pct']}%",
summary["mae"], summary["rmse"], summary["failed_predictions"],
]
def run_backtest_ui(symbol, timeframe, selected_models, window, horizon, step_size,
lookback_days, feature_mode):
empty = pd.DataFrame()
if not selected_models:
return [], empty, empty, empty, empty, empty, "Pick at least one model."
needs_features = feature_mode != FEATURE_MODE_BASELINE
try:
# get_historical_with_features fetches extra real history in front
# (when needs_features) and trims it back off after computing
# indicators on it, so `df`/`features_df` cover exactly
# `lookback_days` with no NaN from insufficient warm-up (see
# data/historical.py's docstring โ€” this replaced a plain
# get_historical_cached() whose first ~20 rows would otherwise be
# unusable for any model needing the front of the tested period).
df, features_df = get_historical_with_features(
symbol, timeframe, lookback_days=int(lookback_days),
use_features=needs_features, refresh=True,
)
except Exception as e:
return [], empty, empty, empty, empty, empty, f"Data error: {e}"
window, horizon, step_size = int(window), int(horizon), int(step_size)
if len(df) < window + horizon + 5:
return [], empty, empty, empty, empty, empty, (
f"Only {len(df)} candles available โ€” raise 'Backtest lookback (days)' "
f"or lower the window size to backtest this symbol/timeframe."
)
# Same asset/symbol data, same timeframe, same horizon, same step, same
# testing period for every model AND every mode below โ€” `df`, `window`,
# `horizon`, `step_size` are fixed once here and reused unchanged, so
# whichever models/modes run are always compared fairly against each
# other, never against a different slice of history.
summary_table = []
detail = {"ARIMA": empty, "Auto-ARIMA": empty, "ARIMA-GARCH": empty,
"Moirai": empty, "TimesFM": empty}
if feature_mode == FEATURE_MODE_COMPARE:
models = {name: (fresh_model(name), fresh_model(name)) for name in selected_models}
try:
per_model_results, per_model_summaries = run_comparison_backtest(
df, models, features_df, window=window, horizon=horizon, step=step_size,
)
except Exception as e:
return [], empty, empty, empty, empty, empty, f"Backtest error: {e}"
for name in selected_models:
for summary in per_model_summaries[name]:
summary_table.append(_summary_row(summary, summary["mode"]))
detail[name] = per_model_results[name]
else:
use_features = feature_mode == FEATURE_MODE_FEATURED
mode_label = FEATURED_LABEL if use_features else BASELINE_LABEL
models = {name: fresh_model(name) for name in selected_models}
try:
per_model_results, per_model_summaries = run_all_models_backtest(
df, models, window=window, horizon=horizon, step=step_size,
features_df=features_df if use_features else None,
)
except Exception as e:
return [], empty, empty, empty, empty, empty, f"Backtest error: {e}"
for name in selected_models:
summary_table.append(_summary_row(per_model_summaries[name], mode_label))
results = per_model_results[name].copy()
results.insert(0, "mode", mode_label)
detail[name] = results
status = (f"Backtested {', '.join(selected_models)} on {len(df)} candles of "
f"{symbol} ({timeframe}) โ€” mode: {feature_mode}.")
return (summary_table, detail["ARIMA"], detail["Auto-ARIMA"], detail["ARIMA-GARCH"],
detail["Moirai"], detail["TimesFM"], status)
# ---------------------------------------------------------------------------
# Tab 4: Live
# ---------------------------------------------------------------------------
LIVE_COLUMNS = ["predicted_at", "mode", "target_time", "current_close", "predicted_close",
"predicted_direction", "actual_close", "actual_direction", "correct"]
def _required_lookback_days(log_df, minimum_days: int = 2) -> int:
"""
How many days back we need to fetch to have a chance of covering every
still-pending prediction's target candle.
BUG FIX: this used to be a hardcoded `lookback_days=2`. If the user
doesn't click "Check Latest" for more than 2 days, the oldest pending
row's `target_time` falls outside that 2-day window entirely --
`row["target_time"] in df.index` is then False FOREVER for that row
(the freshly fetched data never reaches back far enough to contain it),
so it silently stays "pending" forever instead of ever being verified.
That quietly shrinks the accuracy denominator with no error shown --
exactly the kind of silent corruption this project's other "once the
candle closes" fix (see the comment below) was written to prevent, just
from the opposite direction (too-narrow fetch instead of too-early read).
Fix: size the fetch to the age of the oldest pending row (plus a small
safety margin for polling gaps), not a fixed constant. The caller still
clamps this to what the timeframe can actually serve (Yahoo's own
history-depth limit) -- see the clamp in _check_live_impl.
"""
if log_df is None or len(log_df) == 0:
return minimum_days
pending = log_df[log_df["actual_close"].isna()]
if len(pending) == 0:
return minimum_days
oldest_target = pd.to_datetime(pending["target_time"]).min()
now = pd.Timestamp.now(tz=oldest_target.tzinfo)
age_days = (now - oldest_target).total_seconds() / 86400
return max(minimum_days, int(age_days) + 2) # +2 days safety margin
def _check_live_impl(symbol, timeframe, model_name, log_df, use_features=False):
if log_df is None or len(log_df) == 0:
log_df = pd.DataFrame(columns=LIVE_COLUMNS)
else:
log_df = log_df.copy()
# See _required_lookback_days' docstring: fetch far enough back to cover
# the oldest still-pending prediction, not a fixed 2 days. Feature
# warm-up (when use_features) is handled separately, INSIDE
# get_historical_with_features -- it adds its own extra padding on top
# of whatever `fetch_days` this section decides, then trims back off
# (see data/historical.py's docstring) -- so `wanted_days` here is
# purely "how far back do we need for verification", unrelated to
# indicator warm-up. Clamped to what this timeframe's source interval
# can actually serve (e.g. 1m tops out at 7 days on Yahoo) so we never
# request more than yfinance can return -- get_historical_with_features
# clamps internally too, but computing it here lets us tell the user
# when a row is stuck.
source_interval = CUSTOM_TIMEFRAMES.get(timeframe, {}).get("source", timeframe)
max_servable_days = YF_NATIVE_INTERVALS.get(source_interval, {}).get("max_days")
wanted_days = _required_lookback_days(log_df)
fetch_days = min(wanted_days, max_servable_days) if max_servable_days else wanted_days
df, features_df = get_historical_with_features(
symbol, timeframe, lookback_days=fetch_days, use_features=use_features, refresh=True,
)
latest_time = df.index[-1]
latest_close = float(df["Close"].iloc[-1])
# 1) verify any pending prediction whose target candle has now CLOSED.
#
# BUG FIX: the old check was just `row["target_time"] in df.index`. Yahoo's
# intraday feed includes the current, still-forming candle as the last row
# (see data/live.py's own docstring: "the most recent (possibly
# still-forming) candle"), and its Close is just the latest traded price so
# far, not the candle's eventual final close. The instant the target candle
# starts forming it satisfies `in df.index`, so the old code would verify
# right then and there against a mid-candle snapshot -- then never revisit
# that row again, since it's no longer NaN. Reproduced: a prediction that
# was directionally CORRECT against the true close got permanently logged
# as incorrect because the candle had dipped the other way in its first
# minute. That silently corrupts the accuracy % this tab (and
# live_runner.py) reports.
#
# Fix: also require the target candle to no longer be the latest row --
# i.e. a strictly newer candle exists after it -- which is only true once
# it has actually closed.
pending_mask = log_df["actual_close"].isna()
for idx, row in log_df[pending_mask].iterrows():
if row["target_time"] in df.index and row["target_time"] < latest_time:
actual_close = float(df.loc[row["target_time"], "Close"])
actual_direction = "up" if actual_close > row["current_close"] else "down"
log_df.loc[idx, "actual_close"] = actual_close
log_df.loc[idx, "actual_direction"] = actual_direction
log_df.loc[idx, "correct"] = bool(actual_direction == row["predicted_direction"])
# 1b) a pending row whose target_time is older than what THIS timeframe
# can ever serve (e.g. a 1m prediction nobody verified for 10+ days,
# past Yahoo's ~7-day 1m history limit) can never be reached by any
# future fetch either -- fetch_days is already clamped to the max.
# Rather than leave it silently "pending" forever (which would read
# as "still collecting data" indefinitely, hiding the fact that it
# is actually unrecoverable), mark it explicitly unresolvable so
# it's visibly dropped from the log instead of quietly stuck.
if max_servable_days is not None:
cutoff = pd.Timestamp.now(tz=latest_time.tzinfo) - pd.Timedelta(days=max_servable_days)
still_pending = log_df["actual_close"].isna()
unreachable = still_pending & (pd.to_datetime(log_df["target_time"]) < cutoff)
if unreachable.any():
log_df.loc[unreachable, "actual_direction"] = "unresolvable (past history limit)"
log_df.loc[unreachable, "correct"] = None
# Leave actual_close as NaN so summarize-style dropna(subset=["correct"])
# logic elsewhere continues to exclude these from the denominator --
# they were never wrong, they're just permanently unverifiable now.
# 2) make a fresh prediction for the next candle, unless one is already
# pending for this exact latest candle
#
# Dedup by TIMESTAMP (predicted_at == latest_time), not by close price.
# Price equality is the wrong proxy in both directions: an intraday
# "latest" candle from yfinance is often still forming, so its close
# can drift between polls even though it's still the SAME candle
# (price-equality would wrongly log a second, duplicate pending row
# for it before it even closes) -- and a genuinely NEW candle can by
# coincidence close at the exact same price as the previous one,
# especially in a quiet/ranging market (price-equality would then
# wrongly skip predicting it entirely). Either way corrupts the
# running-accuracy count this tab reports. Timestamp equality has
# neither failure mode.
have_pending_for_latest = False
if len(log_df):
last_row = log_df.iloc[-1]
have_pending_for_latest = (
pd.isna(last_row["actual_close"]) and last_row["predicted_at"] == latest_time
)
status_extra = ""
if not have_pending_for_latest:
try:
model = get_model(model_name, symbol, timeframe, use_features=use_features)
# features_df was computed above by get_historical_with_features,
# the exact same feature-generation pipeline backtest uses --
# positionally aligned with df["Close"] by construction, no
# separate windowing step needed here the way backtest_model()
# needs one for its rolling window.
forecast = model.predict(df["Close"], horizon=1, features=features_df)
predicted_close = float(forecast[-1])
predicted_direction = "up" if predicted_close > latest_close else "down"
step = infer_step(df.index)
new_row = {
"predicted_at": latest_time,
"mode": FEATURED_LABEL if use_features else BASELINE_LABEL,
"target_time": latest_time + step,
"current_close": latest_close, "predicted_close": predicted_close,
"predicted_direction": predicted_direction, "actual_close": np.nan,
"actual_direction": None, "correct": None,
}
log_df = pd.concat([log_df, pd.DataFrame([new_row])], ignore_index=True)
except Exception as e:
status_extra = f" (prediction failed: {e})"
verified = log_df.dropna(subset=["correct"])
current_mode = FEATURED_LABEL if use_features else BASELINE_LABEL
# The log can contain rows from BOTH modes if this checkbox was toggled
# between clicks (it's the same running gr.State) -- accuracy is always
# computed for the CURRENT mode's rows only, never blended across modes.
verified_this_mode = verified[verified["mode"] == current_mode]
if len(verified_this_mode):
acc = round(100 * verified_this_mode["correct"].sum() / len(verified_this_mode), 2)
acc_text = (f"Running accuracy for {model_name} [{current_mode}]: {acc}% "
f"({int(verified_this_mode['correct'].sum())}/{len(verified_this_mode)})")
other_count = len(verified) - len(verified_this_mode)
if other_count:
acc_text += f" โ€” {other_count} verified prediction(s) from the other mode are in the table below but not counted here."
else:
acc_text = "Collecting data โ€” check again after the next candle closes to see accuracy."
status = f"Latest {symbol} candle: {latest_time} โ€” Close {latest_close:.5f}. {acc_text}{status_extra}"
return status, log_df.tail(20), log_df
def check_live(symbol, timeframe, model_name, log_df, use_features=False):
try:
return _check_live_impl(symbol, timeframe, model_name, log_df, use_features=use_features)
except Exception as e:
safe_log = log_df if (log_df is not None and len(log_df)) else pd.DataFrame(columns=LIVE_COLUMNS)
return f"Error: {e}", safe_log.tail(20), safe_log
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="Forex & Crypto Prediction Dashboard") as demo:
gr.Markdown(
"# ๐Ÿ“ˆ Forex & Crypto Prediction Dashboard\n"
"ARIMA, Auto-ARIMA, ARIMA-GARCH, Moirai (Salesforce) & TimesFM (Google) โ€” "
"each model's accuracy is tracked and shown **separately**, never averaged "
"together.\n\n" + DISCLAIMER
)
with gr.Tab("๐Ÿ“Š Chart & Indicators"):
with gr.Row():
symbol_in1 = gr.Dropdown(choices=ALL_DEFAULT_SYMBOLS, value="EURUSD=X",
allow_custom_value=True, label="Symbol")
timeframe_in1 = gr.Dropdown(choices=TIMEFRAME_CHOICES, value="15m", label="Timeframe")
lookback_in1 = gr.Slider(1, 60, value=5, step=1, label="Lookback (days)")
fetch_btn = gr.Button("Fetch & Analyze", variant="primary")
chart_out1 = gr.Plot()
signals_out = gr.Dataframe(headers=["Indicator", "Value", "Signal"],
label="Top 5 Indicators (latest reading)")
status_out1 = gr.Markdown()
fetch_btn.click(fetch_and_chart, [symbol_in1, timeframe_in1, lookback_in1],
[chart_out1, signals_out, status_out1])
with gr.Tab("๐Ÿ”ฎ Prediction"):
with gr.Row():
symbol_in2 = gr.Dropdown(choices=ALL_DEFAULT_SYMBOLS, value="BTC-USD",
allow_custom_value=True, label="Symbol")
timeframe_in2 = gr.Dropdown(choices=TIMEFRAME_CHOICES, value="15m", label="Timeframe")
with gr.Row():
models_in2 = gr.CheckboxGroup(choices=MODEL_NAMES, value=["ARIMA"], label="Models")
horizon_in2 = gr.Slider(1, 10, value=1, step=1, label="Candles ahead")
lookback_in2 = gr.Slider(1, 60, value=5, step=1, label="Lookback (days)")
use_features_in2 = gr.Checkbox(value=False, label="Use 10-feature input (OHLCV + RSI/MACD/Bollinger/SMA-EMA/Stochastic)")
gr.Markdown(
"Moirai and TimesFM download pretrained weights the first time you use them "
"(needs internet on the Space; a few hundred MB each, one-time download)."
)
predict_btn = gr.Button("Predict", variant="primary")
chart_out2 = gr.Plot()
summary_out2 = gr.Markdown()
predict_btn.click(run_prediction,
[symbol_in2, timeframe_in2, models_in2, horizon_in2, lookback_in2, use_features_in2],
[chart_out2, summary_out2])
with gr.Tab("๐Ÿงช Backtest"):
gr.Markdown(
"Each model's accuracy is computed and shown **separately** below โ€” "
"results are never averaged together. Raise **Step** to speed up "
"Moirai/TimesFM (slower per-prediction than ARIMA on CPU-only hardware)."
"\n\n" + FEATURES_HELP
)
with gr.Row():
symbol_in3 = gr.Dropdown(choices=ALL_DEFAULT_SYMBOLS, value="EURUSD=X",
allow_custom_value=True, label="Symbol")
timeframe_in3 = gr.Dropdown(choices=TIMEFRAME_CHOICES, value="15m", label="Timeframe")
with gr.Row():
models_in3 = gr.CheckboxGroup(choices=MODEL_NAMES, value=["ARIMA"], label="Models to backtest")
window_in3 = gr.Slider(20, 300, value=60, step=10, label="History window per prediction")
horizon_in3 = gr.Slider(1, 5, value=1, step=1, label="Horizon (candles ahead)")
with gr.Row():
step_in3 = gr.Slider(1, 20, value=1, step=1, label="Step (skip candles between tests)")
lookback_in3 = gr.Slider(1, 60, value=10, step=1, label="Backtest lookback (days)")
feature_mode_in3 = gr.Radio(
choices=[FEATURE_MODE_BASELINE, FEATURE_MODE_FEATURED, FEATURE_MODE_COMPARE],
value=FEATURE_MODE_BASELINE, label="Feature mode",
info="'Compare both' runs every selected model twice โ€” Close-only and +10-feature โ€” on the identical "
"symbol/timeframe/window/horizon/step/period, so you can see whether the extra features actually helped.",
)
backtest_btn = gr.Button("Run Backtest", variant="primary")
summary_out3 = gr.Dataframe(
headers=["Mode", "Model", "Total", "Correct", "Incorrect", "Accuracy", "MAE", "RMSE", "Failed"],
label="Per-model accuracy โ€” kept separate, never mixed",
)
status_out3 = gr.Markdown()
with gr.Tabs():
with gr.Tab("ARIMA โ€” verify log"):
arima_detail = gr.Dataframe(label="Per-candle: predicted vs actual, correct/incorrect")
with gr.Tab("Auto-ARIMA โ€” verify log"):
autoarima_detail = gr.Dataframe(label="Per-candle: predicted vs actual, correct/incorrect")
with gr.Tab("ARIMA-GARCH โ€” verify log"):
arimagarch_detail = gr.Dataframe(label="Per-candle: predicted vs actual, correct/incorrect")
with gr.Tab("Moirai โ€” verify log"):
moirai_detail = gr.Dataframe(label="Per-candle: predicted vs actual, correct/incorrect")
with gr.Tab("TimesFM โ€” verify log"):
timesfm_detail = gr.Dataframe(label="Per-candle: predicted vs actual, correct/incorrect")
backtest_btn.click(
run_backtest_ui,
[symbol_in3, timeframe_in3, models_in3, window_in3, horizon_in3, step_in3, lookback_in3, feature_mode_in3],
[summary_out3, arima_detail, autoarima_detail, arimagarch_detail,
moirai_detail, timesfm_detail, status_out3],
)
with gr.Tab("๐Ÿ“ก Live"):
gr.Markdown(
"Polls Yahoo Finance for the newest candle each time you click the button, "
"predicts the next one, and verifies the previous prediction once that candle "
"has closed. For unattended/continuous logging instead of clicking manually, "
"run `live_runner.py` in the background (see README)."
)
with gr.Row():
symbol_in4 = gr.Dropdown(choices=ALL_DEFAULT_SYMBOLS, value="BTC-USD",
allow_custom_value=True, label="Symbol")
timeframe_in4 = gr.Dropdown(choices=TIMEFRAME_CHOICES, value="5m", label="Timeframe")
model_in4 = gr.Dropdown(choices=MODEL_NAMES, value="ARIMA", label="Model")
use_features_in4 = gr.Checkbox(
value=False, label="Use 10-feature input",
info="Uses the exact same feature-generation pipeline as the Backtest tab. "
"Toggling this mid-session keeps both modes' predictions in the log below, "
"but running accuracy is always computed for the current mode only.",
)
live_btn = gr.Button("๐Ÿ”„ Check Latest & Predict Next", variant="primary")
live_log_state = gr.State(pd.DataFrame(columns=LIVE_COLUMNS))
live_status = gr.Markdown()
live_table = gr.Dataframe(label="Live verification log (this model, this session)")
live_btn.click(check_live, [symbol_in4, timeframe_in4, model_in4, live_log_state, use_features_in4],
[live_status, live_table, live_log_state])
gr.Markdown("---\n" + DISCLAIMER)
if __name__ == "__main__":
demo.launch()