Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 142 additions & 16 deletions toptek/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import tkinter as tk
from tkinter import messagebox, ttk
from typing import Dict
from typing import Any, Dict

import numpy as np

Expand All @@ -22,6 +22,50 @@ def __init__(self, master: ttk.Notebook, configs: Dict[str, Dict[str, object]],
self.configs = configs
self.paths = paths
self.logger = utils.build_logger(self.__class__.__name__)
self._status_palette = {
"info": "#1d4ed8",
"success": "#15803d",
"warning": "#b45309",
"error": "#b91c1c",
"neutral": "",
}

def log_event(self, message: str, *, level: str = "info", exc: Exception | None = None) -> None:
"""Log an event using the tab's logger.

Args:
message: Event description.
level: Logging level name to dispatch (defaults to ``info``).
"""

normalized = level.lower()
if normalized == "warn":
normalized = "warning"
log_method = getattr(self.logger, normalized, self.logger.info)
if exc is not None:
log_method(message, exc_info=exc)
else:
log_method(message)

def update_section(self, section: str, updates: Dict[str, object]) -> None:
"""Update the configuration state for *section* with *updates*."""

section_state = self.configs.setdefault(section, {})
self._deep_update(section_state, updates)

def set_status(self, label: ttk.Label, text: str, *, tone: str = "info") -> None:
"""Apply consistent text/colour styling to status labels."""

palette_key = tone.lower()
foreground = self._status_palette.get(palette_key, self._status_palette["info"])
label.config(text=text, foreground=foreground)

def _deep_update(self, target: Dict[str, Any], updates: Dict[str, Any]) -> None:
for key, value in updates.items():
if isinstance(value, dict) and isinstance(target.get(key), dict):
self._deep_update(target[key], value)
else:
target[key] = value


class LoginTab(BaseTab):
Expand Down Expand Up @@ -60,8 +104,9 @@ def _build(self) -> None:
actions.pack(fill=tk.X, padx=10, pady=(0, 12))
ttk.Button(actions, text="Save .env", command=self._save_env).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(actions, text="Verify entries", command=self._verify_env).pack(side=tk.LEFT)
self.status = ttk.Label(actions, text="Awaiting verification", foreground="#1d4ed8")
self.status = ttk.Label(actions)
self.status.pack(side=tk.LEFT, padx=12)
self.set_status(self.status, "Awaiting verification", tone="info")

def _env_value(self, key: str) -> str:
return os.environ.get(key, "")
Expand All @@ -72,16 +117,16 @@ def _save_env(self) -> None:
for key, var in self.vars.items():
handle.write(f"{key}={var.get()}\n")
messagebox.showinfo("Settings", f"Saved credentials to {env_path}")
self.status.config(text="Saved. Run verification to confirm access.", foreground="#166534")
self.set_status(self.status, "Saved. Run verification to confirm access.", tone="success")

def _verify_env(self) -> None:
missing = [key for key, var in self.vars.items() if not var.get().strip()]
if missing:
details = ", ".join(missing)
self.status.config(text=f"Missing: {details}", foreground="#b91c1c")
self.set_status(self.status, f"Missing: {details}", tone="error")
messagebox.showwarning("Verification", f"Provide values for: {details}")
return
self.status.config(text="All keys present. Proceed to Research ▶", foreground="#15803d")
self.set_status(self.status, "All keys present. Proceed to Research ▶", tone="success")
messagebox.showinfo("Verification", "Environment entries look complete. Continue to the next tab.")


Expand Down Expand Up @@ -198,36 +243,106 @@ def _build(self) -> None:

self.output = tk.Text(self, height=12)
self.output.pack(fill=tk.BOTH, expand=True, padx=10, pady=(6, 4))
self.status = ttk.Label(self, text="Awaiting training run", anchor=tk.W)
self.status = ttk.Label(self, anchor=tk.W)
self.status.pack(fill=tk.X, padx=12, pady=(0, 12))
self.set_status(self.status, "Awaiting training run", tone="info")
self._last_calibration_warning: str | None = None

def _train_model(self) -> None:
try:
lookback = int(self.lookback_var.get())
except (TypeError, ValueError):
lookback = 480
lookback = max(240, min(lookback, 2000))
self.log_event(
f"Training started for {self.model_type.get()} with lookback={lookback}",
level="info",
)
df = sample_dataframe(lookback)
feat_map = features.compute_features(df)
X = np.column_stack(list(feat_map.values()))
y = (np.diff(df["close"], prepend=df["close"].iloc[0]) > 0).astype(int)
result = model.train_classifier(X, y, model_type=self.model_type.get(), models_dir=self.paths.models)
calibrate_report = "skipped"
if self.calibrate_var.get() and len(X) > 60:
cal_size = max(60, int(len(X) * 0.2))
X_cal = X[-cal_size:]
y_cal = y[-cal_size:]
calibrated_path = model.calibrate_classifier(result.model_path, (X_cal, y_cal))
calibrate_report = f"calibrated → {calibrated_path.name}"
calibration_failed = False
calibrated_model: str | None = None
if self.calibrate_var.get():
calibration_status = "pending"
calibrate_report = "calibration pending"
if len(X) > 60:
cal_size = max(60, int(len(X) * 0.2))
X_cal = X[-cal_size:]
y_cal = y[-cal_size:]
try:
calibrated_path = model.calibrate_classifier(result.model_path, (X_cal, y_cal))
except (ValueError, RuntimeError, OSError) as exc:
calibrate_report = f"skipped calibration · {type(exc).__name__}: {exc}".strip()
calibration_status = "skipped"
calibration_failed = True
self.log_event(
f"Calibration failed for {result.model_path.name}: {type(exc).__name__}: {exc}",
level="warning",
exc=exc,
)
self.set_status(
self.status,
"Calibration skipped due to data quality. Review logs for details.",
tone="error",
)
warning_body = (
"Probability calibration failed with the current sample. "
"The base model artefact is saved without calibration.\n\n"
f"Details: {type(exc).__name__}: {exc}"
)
if warning_body != self._last_calibration_warning:
messagebox.showwarning("Calibration warning", warning_body)
self._last_calibration_warning = warning_body
else:
calibrated_model = calibrated_path.name
calibrate_report = f"calibrated → {calibrated_model}"
calibration_status = "success"
self._last_calibration_warning = None
self.log_event(
f"Calibration completed for {result.model_path.name} → {calibrated_model}",
level="info",
)
else:
calibration_status = "skipped"
calibrate_report = "skipped calibration · insufficient holdout samples"
self._last_calibration_warning = None
self.log_event(
f"Calibration skipped for {result.model_path.name}: insufficient holdout samples ({len(X)})",
level="info",
)
else:
calibration_status = "skipped"
calibrate_report = "skipped calibration · user preference"
self._last_calibration_warning = None
self.log_event(
f"Calibration not requested for {result.model_path.name}",
level="info",
)
self.output.delete("1.0", tk.END)
payload = {
"model": self.model_type.get(),
"metrics": result.metrics,
"threshold": result.threshold,
"model_path": result.model_path.name,
"calibration": calibrate_report,
"calibration_status": "skipped" if calibration_failed else calibration_status,
"calibrated_model": calibrated_model,
}
self.output.insert(tk.END, json_dumps(payload))
self.status.config(text="Model artefact refreshed. Continue to Backtest ▶")
self.update_section("training", payload)
if not calibration_failed:
self.set_status(
self.status,
"Model artefact refreshed. Continue to Backtest ▶",
tone="success",
)
self.log_event(
f"Training completed for {result.model_path.name} (calibration={payload['calibration_status']})",
level="info",
)


class BacktestTab(BaseTab):
Expand Down Expand Up @@ -271,8 +386,9 @@ def _build(self) -> None:

self.output = tk.Text(self, height=14)
self.output.pack(fill=tk.BOTH, expand=True, padx=10, pady=(6, 4))
self.status = ttk.Label(self, text="No simulations yet", anchor=tk.W)
self.status = ttk.Label(self, anchor=tk.W)
self.status.pack(fill=tk.X, padx=12, pady=(0, 12))
self.set_status(self.status, "No simulations yet", tone="info")

def _run_backtest(self) -> None:
try:
Expand All @@ -298,7 +414,11 @@ def _run_backtest(self) -> None:
}
self.output.delete("1.0", tk.END)
self.output.insert(tk.END, json_dumps(payload))
self.status.config(text="Sim complete. If expectancy holds, draft a manual trade plan ▶")
self.set_status(
self.status,
"Sim complete. If expectancy holds, draft a manual trade plan ▶",
tone="success",
)


class TradeTab(BaseTab):
Expand Down Expand Up @@ -357,6 +477,12 @@ def _show_risk(self) -> None:
}
self.output.delete("1.0", tk.END)
self.output.insert(tk.END, json_dumps(payload))
guard_message = (
"Guard status OK — within limits. Proceed only with fully vetted manual plans."
if guard == "OK"
else "Topstep guard engaged DEFENSIVE_MODE. Stand down and review journal before trading."
)
messagebox.showinfo("Topstep guard", guard_message)


__all__ = [
Expand Down