import os import queue import threading import tkinter as tk from datetime import datetime from tkinter import messagebox, ttk from config import (DEFAULT_INSTRUMENTS, load_config, load_sent, save_config, save_sent) from dtc_client import DTCClient, fills_to_trades, monitor_realtime from tralgo_api import send_trade, send_trades_bulk try: from PIL import Image, ImageTk _PIL_AVAILABLE = True except ImportError: _PIL_AVAILABLE = False _ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.png") _ICON_FULL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon_full.png") def _load_icon(path, size): """Open icon, crop transparent padding, add 8 % margin, resize to size×size.""" img = Image.open(path).convert("RGBA") bbox = img.getbbox() if bbox: img = img.crop(bbox) m = max(img.width, img.height) // 12 # ~8 % margin canvas_sz = max(img.width, img.height) + 2 * m canvas = Image.new("RGBA", (canvas_sz, canvas_sz), (0, 0, 0, 0)) ox = (canvas_sz - img.width) // 2 oy = (canvas_sz - img.height) // 2 canvas.paste(img, (ox, oy), img) return canvas.resize((size, size), Image.LANCZOS) # ── Config window ───────────────────────────────────────────────────────────── class ConfigWindow: _PRESETS = list(DEFAULT_INSTRUMENTS.keys()) # MNQ, NQ, ES, MES def __init__(self, parent, config, on_save): self.win = tk.Toplevel(parent) self.win.title("Einstellungen – GizerBridge") self.win.resizable(False, False) self.win.grab_set() self._config = config self._on_save = on_save self._inst_vars = {} self._inst_frame = None self._build() self.win.protocol("WM_DELETE_WINDOW", self.win.destroy) def _build(self): cfg = self._config pad = {"padx": 10, "pady": 3} self._api_key_var = self._labeled_entry("API Key *", cfg.get("api_key", ""), **pad) self._dtc_host_var = self._labeled_entry( "SC DTC Host", cfg.get("dtc_host", "localhost"), **pad) row = tk.Frame(self.win) row.pack(fill=tk.X, **pad) tk.Label(row, text="SC DTC Port", width=20, anchor="w").pack(side=tk.LEFT) self._dtc_port_var = tk.StringVar(value=str(cfg.get("dtc_port", 11099))) tk.Entry(row, textvariable=self._dtc_port_var, width=8).pack(side=tk.LEFT) tk.Label(row, text="(Sierra Chart → DTC Protocol Server aktivieren)", fg="gray", font=("", 8)).pack(side=tk.LEFT, padx=(8, 0)) tk.Label(self.win, text="Konten:", anchor="w", font=("", 9, "bold")).pack(fill=tk.X, padx=10, pady=(10, 2)) acc_outer = tk.Frame(self.win) acc_outer.pack(fill=tk.X, padx=10) self._acc_listbox = tk.Listbox(acc_outer, height=4, width=48, selectmode=tk.SINGLE) self._acc_listbox.pack(side=tk.LEFT) for acc in cfg.get("accounts", []): self._acc_listbox.insert(tk.END, acc) add_row = tk.Frame(self.win) add_row.pack(fill=tk.X, padx=10, pady=2) self._acc_entry = tk.Entry(add_row, width=35) self._acc_entry.pack(side=tk.LEFT) self._acc_entry.bind("", lambda _: self._add_account()) tk.Button(add_row, text="Hinzufügen", command=self._add_account).pack(side=tk.LEFT, padx=4) tk.Button(add_row, text="Entfernen", command=self._remove_account).pack(side=tk.LEFT) tk.Label(self.win, text="Instrumente:", anchor="w", font=("", 9, "bold")).pack(fill=tk.X, padx=10, pady=(10, 2)) outer = tk.LabelFrame(self.win, text="") outer.pack(fill=tk.X, padx=10, pady=2) hdr = tk.Frame(outer) hdr.pack(fill=tk.X, padx=4, pady=(4, 0)) tk.Label(hdr, text="", width=3).pack(side=tk.LEFT) tk.Label(hdr, text="Symbol", width=10, anchor="w").pack(side=tk.LEFT) tk.Label(hdr, text="Tick-Größe", width=11, anchor="w").pack(side=tk.LEFT) tk.Label(hdr, text="Tick-Wert $",width=11, anchor="w").pack(side=tk.LEFT) self._inst_frame = tk.Frame(outer) self._inst_frame.pack(fill=tk.X) existing = cfg.get("instruments", {}) rendered = set() for name in self._PRESETS: self._add_inst_row(name, existing.get(name, DEFAULT_INSTRUMENTS.get(name, {}))) rendered.add(name) for name, data in existing.items(): if name not in rendered: self._add_inst_row(name, data) custom = tk.Frame(outer) custom.pack(fill=tk.X, padx=4, pady=6) tk.Label(custom, text="Weiteres:").pack(side=tk.LEFT) self._custom_name = tk.Entry(custom, width=7) self._custom_name.pack(side=tk.LEFT, padx=(3, 6)) tk.Label(custom, text="Tick-Größe:").pack(side=tk.LEFT) self._custom_tick_size = tk.Entry(custom, width=6) self._custom_tick_size.insert(0, "0.25") self._custom_tick_size.pack(side=tk.LEFT, padx=(2, 6)) tk.Label(custom, text="Tick-Wert $:").pack(side=tk.LEFT) self._custom_tick_value = tk.Entry(custom, width=6) self._custom_tick_value.pack(side=tk.LEFT, padx=2) tk.Button(custom, text="+", width=3, command=self._add_custom_inst).pack(side=tk.LEFT, padx=4) btn = tk.Frame(self.win) btn.pack(pady=10) tk.Button(btn, text="Speichern", width=12, command=self._save).pack(side=tk.LEFT, padx=8) tk.Button(btn, text="Abbrechen", width=12, command=self.win.destroy).pack(side=tk.LEFT) def _labeled_entry(self, label, value, **pack_kwargs): row = tk.Frame(self.win) row.pack(fill=tk.X, **pack_kwargs) tk.Label(row, text=label, width=20, anchor="w").pack(side=tk.LEFT) var = tk.StringVar(value=value) tk.Entry(row, textvariable=var, width=40).pack(side=tk.LEFT) return var def _add_inst_row(self, name, data): if name in self._inst_vars: return row = tk.Frame(self._inst_frame) row.pack(fill=tk.X, padx=4, pady=1) preset_defaults = DEFAULT_INSTRUMENTS.get(name, {}) enabled_var = tk.BooleanVar(value=data.get("enabled", name in self._PRESETS)) tick_size_var = tk.StringVar(value=str(data.get("tick_size", preset_defaults.get("tick_size", 0.25)))) tick_value_var = tk.StringVar(value=str(data.get("tick_value", preset_defaults.get("tick_value", 0.0)))) tk.Checkbutton(row, variable=enabled_var).pack(side=tk.LEFT) tk.Label(row, text=name, width=10, anchor="w").pack(side=tk.LEFT) tk.Entry(row, textvariable=tick_size_var, width=9).pack(side=tk.LEFT, padx=2) tk.Entry(row, textvariable=tick_value_var, width=9).pack(side=tk.LEFT, padx=2) self._inst_vars[name] = { "enabled": enabled_var, "tick_size": tick_size_var, "tick_value": tick_value_var, } def _add_custom_inst(self): name = self._custom_name.get().strip().upper() if not name: return if name in self._inst_vars: messagebox.showinfo("Hinweis", f"{name} ist bereits vorhanden.", parent=self.win) return try: ts = float(self._custom_tick_size.get()) tv = float(self._custom_tick_value.get()) except ValueError: messagebox.showerror("Fehler", "Ungültige Zahlenwerte.", parent=self.win) return self._add_inst_row(name, {"enabled": True, "tick_size": ts, "tick_value": tv}) self._custom_name.delete(0, tk.END) self._custom_tick_value.delete(0, tk.END) def _add_account(self): name = self._acc_entry.get().strip() if name and name not in self._acc_listbox.get(0, tk.END): self._acc_listbox.insert(tk.END, name) self._acc_entry.delete(0, tk.END) def _remove_account(self): sel = self._acc_listbox.curselection() if sel: self._acc_listbox.delete(sel[0]) def _save(self): api_key = self._api_key_var.get().strip() if not api_key: messagebox.showerror("Fehler", "API Key ist ein Pflichtfeld.", parent=self.win) return try: dtc_port = int(self._dtc_port_var.get().strip()) except ValueError: messagebox.showerror("Fehler", "DTC Port muss eine Zahl sein.", parent=self.win) return instruments = {} for name, v in self._inst_vars.items(): try: ts = float(v["tick_size"].get()) tv = float(v["tick_value"].get()) except ValueError: messagebox.showerror("Fehler", f"Ungültige Tick-Werte für {name}.", parent=self.win) return instruments[name] = { "enabled": v["enabled"].get(), "tick_size": ts, "tick_value": tv, } self._on_save({ "api_key": api_key, "dtc_host": self._dtc_host_var.get().strip() or "localhost", "dtc_port": dtc_port, "accounts": list(self._acc_listbox.get(0, tk.END)), "instruments": instruments, }) self.win.destroy() # ── Manual import window ────────────────────────────────────────────────────── class ManualImportWindow: _CHK_ON = "☑" _CHK_OFF = "☐" _COLS = ("chk", "date", "account", "instrument", "direction", "entry", "exit", "pnl", "ticks", "status") _HDRS = ("", "Datum", "Konto", "Symbol", "Richtung", "Entry", "Exit", "PnL $", "Ticks", "Status") _WIDTHS = (30, 100, 140, 65, 70, 80, 80, 70, 55, 70) def __init__(self, parent, config, sent, api_key, on_imported, on_reset): self.win = tk.Toplevel(parent) self.win.title("Manueller Import – GizerBridge") self.win.geometry("870x520") self.win.resizable(True, True) self._config = config self._sent = sent # set of trade IDs already sent self._api_key = api_key self._on_imported = on_imported # callback(trade_id) → updates App.sent self._on_reset = on_reset # callback(trade_id) → removes from App.sent self._rows = [] # [(iid, trade_dict, is_sent)] self._build() def _build(self): # ── Filter bar ──────────────────────────────────────────────────────── ctrl = tk.Frame(self.win) ctrl.pack(fill=tk.X, padx=10, pady=(10, 4)) tk.Label(ctrl, text="Konto:").pack(side=tk.LEFT) accounts = ["Alle"] + list(self._config.get("accounts", [])) self._account_var = tk.StringVar(value=accounts[0] if accounts else "") ttk.Combobox(ctrl, textvariable=self._account_var, values=accounts, width=22, state="readonly").pack(side=tk.LEFT, padx=(4, 14)) tk.Label(ctrl, text="Von:").pack(side=tk.LEFT) self._date_from_var = tk.StringVar(value="2026-01-01") tk.Entry(ctrl, textvariable=self._date_from_var, width=11).pack(side=tk.LEFT, padx=(4, 14)) tk.Label(ctrl, text="Bis:").pack(side=tk.LEFT) self._date_to_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d")) tk.Entry(ctrl, textvariable=self._date_to_var, width=11).pack(side=tk.LEFT, padx=(4, 14)) self._search_btn = tk.Button(ctrl, text="Suchen", command=self._search) self._search_btn.pack(side=tk.LEFT) self._count_lbl = tk.Label(ctrl, text="", fg="gray") self._count_lbl.pack(side=tk.LEFT, padx=10) tk.Label(self.win, text="ℹ Timestamps direkt aus SC via DTC — echte Füll-Zeiten.", fg="#888888", font=("", 8), anchor="w", ).pack(fill=tk.X, padx=11, pady=(0, 2)) # ── Treeview ────────────────────────────────────────────────────────── tree_outer = tk.Frame(self.win) tree_outer.pack(fill=tk.BOTH, expand=True, padx=10, pady=4) vsb = ttk.Scrollbar(tree_outer, orient="vertical") hsb = ttk.Scrollbar(tree_outer, orient="horizontal") vsb.pack(side=tk.RIGHT, fill=tk.Y) hsb.pack(side=tk.BOTTOM, fill=tk.X) self._tree = ttk.Treeview( tree_outer, columns=self._COLS, show="headings", yscrollcommand=vsb.set, xscrollcommand=hsb.set, selectmode="none", ) vsb.config(command=self._tree.yview) hsb.config(command=self._tree.xview) for col, hdr, w in zip(self._COLS, self._HDRS, self._WIDTHS): anchor = "e" if col in ("entry", "exit", "pnl", "ticks") else "w" self._tree.heading(col, text=hdr, anchor=anchor) self._tree.column(col, width=w, minwidth=w, stretch=(col == "account"), anchor=anchor) self._tree.tag_configure("sent", foreground="#aaaaaa") self._tree.tag_configure("sel", foreground="#000000", background="#e8f4e8") self._tree.tag_configure("desel", foreground="#333333") self._tree.tag_configure("ok", foreground="darkgreen", background="#e8f4e8") self._tree.tag_configure("err", foreground="#cc0000") self._tree.bind("", self._on_click) self._tree.pack(fill=tk.BOTH, expand=True) # ── Bottom bar ──────────────────────────────────────────────────────── bot = tk.Frame(self.win) bot.pack(fill=tk.X, padx=10, pady=(4, 10)) tk.Button(bot, text="Alle auswählen", command=self._select_all).pack(side=tk.LEFT, padx=(0, 4)) tk.Button(bot, text="Keine", command=self._select_none).pack(side=tk.LEFT, padx=4) self._reset_btn = tk.Button(bot, text="Import zurücksetzen", command=self._reset_sent, state=tk.DISABLED) self._reset_btn.pack(side=tk.LEFT, padx=4) self._status_lbl = tk.Label(bot, text="", fg="gray") self._status_lbl.pack(side=tk.LEFT, padx=12) self._import_btn = tk.Button( bot, text="Ausgewählte importieren", command=self._import_selected, state=tk.DISABLED, ) self._import_btn.pack(side=tk.RIGHT) # ── Search ──────────────────────────────────────────────────────────────── def _search(self): self._tree.delete(*self._tree.get_children()) self._rows.clear() self._import_btn.config(state=tk.DISABLED) self._count_lbl.config(text="Verbinde mit SC DTC…") self._status_lbl.config(text="") self._search_btn.config(state=tk.DISABLED) self.win.update_idletasks() account_sel = self._account_var.get() date_from = self._date_from_var.get().strip() date_to = self._date_to_var.get().strip() cfg_accounts = self._config.get("accounts", []) accounts = cfg_accounts if account_sel == "Alle" else [account_sel] instruments = {k: v for k, v in self._config.get("instruments", {}).items() if v.get("enabled", True)} host = self._config.get("dtc_host", "localhost") port = self._config.get("dtc_port", 11099) def _run(): all_fills = [] dtc_error = None try: client = DTCClient(host, port) client.connect() client.logon() for account in accounts: fills = client.request_fills(account, num_days=90) all_fills.extend(fills) client.disconnect() except Exception as e: dtc_error = str(e) if dtc_error and not all_fills: self.win.after(0, lambda: self._search_done([], dtc_error)) return filtered_fills = [] for f in all_fills: if not f.get("datetime"): filtered_fills.append(f) continue d = f["datetime"].strftime("%Y-%m-%d") if date_from and d < date_from: continue if date_to and d > date_to: continue filtered_fills.append(f) all_trades = fills_to_trades(filtered_fills, instruments) err = dtc_error if not all_trades else None self.win.after(0, lambda: self._search_done(all_trades, err)) threading.Thread(target=_run, daemon=True).start() def _search_done(self, trades, error_msg): self._search_btn.config(state=tk.NORMAL) if error_msg: self._count_lbl.config(text=f"Verbindungsfehler: {error_msg[:60]}") return total = 0 for trade in trades: is_sent = trade["trade_id"] in self._sent tag = "sent" if is_sent else "desel" pnl_fmt = f"{trade['pnl']:+.2f}" if trade["pnl"] != 0 else "0.00" date_str = trade["entry_datetime"][:10] if trade["entry_datetime"] else "" iid = self._tree.insert( "", tk.END, values=( self._CHK_OFF, date_str, trade["account"], trade["instrument"], trade["direction"], trade["entry_price"], trade["exit_price"], pnl_fmt, trade["pnl_ticks"], "bereits importiert" if is_sent else "", ), tags=(tag,), ) self._rows.append((iid, trade, is_sent)) total += 1 already = sum(1 for _, _, s in self._rows if s) parts = [f"{total} Trade(s) gefunden"] if already: parts.append(f"{already} bereits importiert") self._count_lbl.config(text=" – ".join(parts)) self._refresh_import_btn() # ── Checkbox interaction ────────────────────────────────────────────────── def _on_click(self, event): iid = self._tree.identify_row(event.y) if not iid or self._tree.identify_region(event.x, event.y) != "cell": return for row_iid, trade, is_sent in self._rows: if row_iid != iid: continue if is_sent: return vals = list(self._tree.item(iid, "values")) if vals[0] == self._CHK_OFF: vals[0] = self._CHK_ON self._tree.item(iid, values=vals, tags=("sel",)) else: vals[0] = self._CHK_OFF self._tree.item(iid, values=vals, tags=("desel",)) break self._refresh_import_btn() def _select_all(self): for iid, _, is_sent in self._rows: if is_sent: continue vals = list(self._tree.item(iid, "values")) vals[0] = self._CHK_ON self._tree.item(iid, values=vals, tags=("sel",)) self._refresh_import_btn() def _select_none(self): for iid, _, is_sent in self._rows: vals = list(self._tree.item(iid, "values")) vals[0] = self._CHK_OFF self._tree.item(iid, values=vals, tags=("sent" if is_sent else "desel",)) self._refresh_import_btn() def _refresh_import_btn(self): any_checked = any( self._tree.item(iid, "values")[0] == self._CHK_ON for iid, _, _ in self._rows ) self._import_btn.config(state=tk.NORMAL if any_checked else tk.DISABLED) any_sent = any(is_sent for _, _, is_sent in self._rows) self._reset_btn.config(state=tk.NORMAL if any_sent else tk.DISABLED) # ── Reset sent ──────────────────────────────────────────────────────────── def _reset_sent(self): if not messagebox.askyesno( "Import zurücksetzen", "Bereits importierte Trades zurücksetzen?\n" "Sie können danach erneut importiert werden.", parent=self.win, ): return updated = [] for iid, trade, is_sent in self._rows: if is_sent: self._on_reset(trade["trade_id"]) vals = list(self._tree.item(iid, "values")) vals[0] = self._CHK_OFF vals[9] = "" self._tree.item(iid, values=vals, tags=("desel",)) updated.append((iid, trade, False)) else: updated.append((iid, trade, is_sent)) self._rows = updated self._refresh_import_btn() # ── Import ──────────────────────────────────────────────────────────────── def _import_selected(self): to_send = [ (iid, trade) for iid, trade, _ in self._rows if self._tree.item(iid, "values")[0] == self._CHK_ON ] if not to_send: messagebox.showinfo("Hinweis", "Keine Trades ausgewählt.", parent=self.win) return self._import_btn.config(state=tk.DISABLED) self._search_btn.config(state=tk.DISABLED) self._status_lbl.config(text=f"Importiere {len(to_send)} Trade(s)…") def _run(): trades_list = [trade for _, trade in to_send] try: send_trades_bulk(self._api_key, trades_list) for iid, trade in to_send: self._on_imported(trade["trade_id"]) self.win.after(0, lambda i=iid, t=trade: self._mark_ok(i, t)) summary = f"Fertig: {len(to_send)} importiert" except Exception as e: msg = str(e) for iid, _ in to_send: self.win.after(0, lambda i=iid, m=msg: self._mark_err(i, m)) summary = f"Fehler: {msg[:60]}" self.win.after(0, lambda: self._import_btn.config(state=tk.NORMAL)) self.win.after(0, lambda: self._status_lbl.config(text=summary)) self.win.after(0, lambda: self._search_btn.config(state=tk.NORMAL)) threading.Thread(target=_run, daemon=True).start() def _mark_ok(self, iid, trade): vals = list(self._tree.item(iid, "values")) vals[0] = self._CHK_ON vals[9] = "✅" self._tree.item(iid, values=vals, tags=("ok",)) def _mark_err(self, iid, msg): vals = list(self._tree.item(iid, "values")) vals[9] = f"❌ {msg[:30]}" self._tree.item(iid, values=vals, tags=("err",)) # ── Main App ────────────────────────────────────────────────────────────────── class App: def __init__(self): self.root = tk.Tk() self.root.withdraw() self.root.title("GizerBridge") self.root.tk.call("wm", "iconname", self.root, "gizerbridge") if _PIL_AVAILABLE: try: self._wm_icons = [ ImageTk.PhotoImage(_load_icon(_ICON_PATH, sz)) for sz in (256, 128, 64, 48, 32) ] self.root.iconphoto(True, *self._wm_icons) except Exception: pass self.config = load_config() self.sent = load_sent() self._status_color = "yellow" self._status_msg = "Initialisierung..." self._log_entries = [] self._log_lock = threading.Lock() self._stop = threading.Event() self._queue = queue.Queue() self._monitor_thread = None # ── Public entry point ──────────────────────────────────────────────────── def run(self): self.root.after(200, self._process_queue) if not self.config.get("api_key"): self.root.after(100, lambda: ConfigWindow( self.root, self.config, self._on_config_saved)) else: self._start_monitor() self._show_main_window() self.root.mainloop() def _set_icon_color(self, color): self._status_color = color # ── Main window ─────────────────────────────────────────────────────────── def _show_main_window(self): if hasattr(self, "_fallback_status"): return self.root.deiconify() self.root.title("GizerBridge") self.root.resizable(True, True) self.root.minsize(420, 140) # ── Header ── hdr = tk.Frame(self.root, padx=14, pady=10) hdr.pack(fill=tk.X) if _PIL_AVAILABLE: try: self._hdr_photo = ImageTk.PhotoImage(_load_icon(_ICON_FULL_PATH, 128)) tk.Label(hdr, image=self._hdr_photo).pack(side=tk.LEFT) except Exception: tk.Label(hdr, text="GizerBridge", font=("", 13, "bold")).pack(side=tk.LEFT) else: tk.Label(hdr, text="GizerBridge", font=("", 13, "bold")).pack(side=tk.LEFT) # ── Status row ── sf = tk.Frame(self.root, padx=14) sf.pack(fill=tk.X) self._fallback_dot = tk.Label(sf, text="●", fg="goldenrod", font=("", 11)) self._fallback_dot.pack(side=tk.LEFT) self._fallback_status = tk.Label(sf, text=self._status_msg, anchor="w", wraplength=360) self._fallback_status.pack(side=tk.LEFT, padx=(5, 0)) # ── Buttons ── bf = tk.Frame(self.root, padx=14, pady=12) bf.pack(fill=tk.X) for text, cmd in [ ("Manueller Import", self._open_manual_import), ("Einstellungen", self._open_config), ("Log", self._open_log), ("Beenden", self._quit), ]: tk.Button(bf, text=text, width=15, command=cmd).pack(side=tk.LEFT, padx=(0, 6)) self.root.protocol("WM_DELETE_WINDOW", self._quit) # ── Monitor thread ──────────────────────────────────────────────────────── def _start_monitor(self): self._stop.clear() self._monitor_thread = threading.Thread( target=self._monitor_loop, daemon=True) self._monitor_thread.start() def _monitor_loop(self): self._queue.put(lambda: self._set_icon_color("yellow")) cfg = self.config if not cfg.get("api_key"): self._log("Konfiguration unvollständig — bitte Einstellungen öffnen.") self._queue.put(lambda: self._set_icon_color("red")) return host = cfg.get("dtc_host", "localhost") port = cfg.get("dtc_port", 11099) accounts = cfg.get("accounts", []) instruments = self._active_instruments(cfg) def _on_trade(trade): if trade["trade_id"] in self.sent: return try: send_trade(cfg["api_key"], trade) self.sent.add(trade["trade_id"]) save_sent(self.sent) msg = (f"Übertragen: {trade['instrument']} {trade['direction']} " f"Entry {trade['entry_price']} → Exit {trade['exit_price']} " f"| PnL {trade['pnl']} USD ({trade['pnl_ticks']} Ticks)") self._log(msg) self._update_status_msg(msg) self._queue.put(lambda: self._set_icon_color("green")) except Exception as e: err_msg = f"Übertragungsfehler ({trade['instrument']}): {e}" self._log(err_msg) self._update_status_msg(err_msg) self._queue.put(lambda: self._set_icon_color("red")) if not accounts: self._log("Keine Konten konfiguriert.") return self._log(f"Verbinde mit Sierra Chart DTC {host}:{port}…") def _on_ready(existing_ids): self.sent.update(existing_ids) save_sent(self.sent) self._log("Bereit. Überwache SC via DTC auf neue Trades…") self._queue.put(lambda: self._set_icon_color("green")) self._update_status_msg("Läuft – warte auf neue Trades (DTC)") result = monitor_realtime( host, port, accounts, instruments, on_trade=_on_trade, stop_event=self._stop, log_fn=self._log, on_ready=_on_ready, ) if result is None: self._queue.put(lambda: self._set_icon_color("red")) self._update_status_msg("DTC-Verbindung fehlgeschlagen – Sierra Chart läuft?") else: self._log("DTC-Monitor beendet.") @staticmethod def _active_instruments(cfg): return {k: v for k, v in cfg.get("instruments", {}).items() if v.get("enabled", True)} # ── Queue / status helpers ──────────────────────────────────────────────── def _process_queue(self): try: while True: self._queue.get_nowait()() except queue.Empty: pass if hasattr(self, "_fallback_status"): lbl_fg = {"green": "darkgreen", "red": "#cc0000", "yellow": "darkorange"} dot_fg = {"green": "#00b300", "red": "#cc0000", "yellow": "goldenrod"} self._fallback_status.config( text=self._status_msg, fg=lbl_fg.get(self._status_color, "gray")) self._fallback_dot.config( fg=dot_fg.get(self._status_color, "gray")) self.root.after(300, self._process_queue) def _log(self, msg): entry = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}" with self._log_lock: self._log_entries.append(entry) if len(self._log_entries) > 200: self._log_entries.pop(0) def _update_status_msg(self, msg): self._status_msg = msg # ── GUI windows ─────────────────────────────────────────────────────────── def _open_status(self): win = tk.Toplevel(self.root) win.title("Status") win.resizable(False, False) colors = {"green": "darkgreen", "red": "red", "yellow": "darkorange"} labels = {"green": "Läuft", "red": "Fehler", "yellow": "Initialisierung"} tk.Label(win, text=f"● {labels.get(self._status_color, '')}", fg=colors.get(self._status_color, "gray"), font=("", 12, "bold")).pack(padx=20, pady=(14, 4)) tk.Label(win, text=self._status_msg, wraplength=380).pack(padx=20, pady=4) tk.Label(win, text=f"Übertragene Trades gesamt: {len(self.sent)}", fg="gray").pack(padx=20, pady=4) with self._log_lock: last = self._log_entries[-1] if self._log_entries else "—" tk.Label(win, text=f"Letzter Log-Eintrag:\n{last}", wraplength=380, fg="gray", justify=tk.LEFT).pack(padx=20, pady=(4, 14)) tk.Button(win, text="Schließen", width=10, command=win.destroy).pack(pady=(0, 12)) def _open_log(self): win = tk.Toplevel(self.root) win.title("Log – letzte 50 Einträge") win.geometry("620x400") frame = tk.Frame(win) frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8) sb = tk.Scrollbar(frame) sb.pack(side=tk.RIGHT, fill=tk.Y) txt = tk.Text(frame, yscrollcommand=sb.set, state=tk.DISABLED, font=("Monospace", 8), wrap=tk.NONE) txt.pack(fill=tk.BOTH, expand=True) sb.config(command=txt.yview) with self._log_lock: entries = list(self._log_entries[-50:]) txt.config(state=tk.NORMAL) txt.insert(tk.END, "\n".join(entries)) txt.config(state=tk.DISABLED) txt.see(tk.END) tk.Button(win, text="Schließen", command=win.destroy).pack(pady=6) def _open_config(self): ConfigWindow(self.root, self.config, self._on_config_saved) def _open_manual_import(self): ManualImportWindow( self.root, self.config, self.sent, self.config.get("api_key", ""), self._on_trade_manually_imported, self._on_trade_reset, ) def _on_trade_manually_imported(self, trade_id): self.sent.add(trade_id) save_sent(self.sent) def _on_trade_reset(self, trade_id): self.sent.discard(trade_id) save_sent(self.sent) def _on_config_saved(self, new_cfg): self.config = new_cfg save_config(new_cfg) self._stop.set() if self._monitor_thread and self._monitor_thread.is_alive(): self._monitor_thread.join(timeout=5) self._start_monitor() def _quit(self): self._stop.set() save_sent(self.sent) try: self.root.quit() self.root.destroy() except tk.TclError: pass