initial release: GizerBridge v1.0

This commit is contained in:
2026-05-22 12:27:27 +02:00
commit b2e4a8b46a
23 changed files with 2773 additions and 0 deletions
+842
View File
@@ -0,0 +1,842 @@
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 cookie_reader import find_cookies
from dtc_client import DTCClient, fills_to_trades, monitor_realtime
from tralgo_api import build_session, send_trade
try:
from PIL import Image, ImageDraw, ImageTk
_PIL_AVAILABLE = True
except ImportError:
_PIL_AVAILABLE = False
try:
import pystray
_PYSTRAY_AVAILABLE = True
except ImportError:
_PYSTRAY_AVAILABLE = False
_ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.png")
# ── 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._email_var = self._labeled_entry("Tralgo E-Mail", cfg.get("email", ""), **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("<Return>", 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,
"email": self._email_var.get().strip(),
"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, session, api_key, on_imported):
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._session = session # requests.Session or None
self._api_key = api_key
self._on_imported = on_imported # callback(trade_id) → updates 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("<ButtonRelease-1>", 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._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 = []
error_msg = 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:
error_msg = str(e)
if error_msg:
self.win.after(0, lambda: self._search_done([], error_msg))
return
# Filter by date range
filtered = []
for f in all_fills:
if not f.get("datetime"):
filtered.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.append(f)
trades = fills_to_trades(filtered, instruments)
self.win.after(0, lambda: self._search_done(trades, None))
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))
# ── 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
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=("sent" if is_sent else "desel",))
break
self._refresh_import_btn()
def _select_all(self):
for iid, _, is_sent in self._rows:
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)
# ── Import ────────────────────────────────────────────────────────────────
def _import_selected(self):
if self._session is None:
messagebox.showerror(
"Keine Verbindung",
"Keine aktive Tralgo-Sitzung.\n"
"Bitte Firefox-Cookie prüfen und App neu starten.",
parent=self.win,
)
return
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():
ok = err = 0
for iid, trade in to_send:
try:
send_trade(self._session, trade, self._api_key)
self._on_imported(trade["trade_id"])
ok += 1
self.win.after(0, lambda i=iid, t=trade: self._mark_ok(i, t))
except Exception as e:
err += 1
self.win.after(0, lambda i=iid, msg=str(e): self._mark_err(i, msg))
summary = f"Fertig: {ok} importiert"
if err:
summary += f", {err} Fehler"
self.win.after(0, lambda: self._status_lbl.config(text=summary))
self.win.after(0, lambda: self._search_btn.config(state=tk.NORMAL))
# Re-enable import for any rows that still failed
remaining = any(
not is_sent and self._tree.item(r_iid, "values")[0] == self._CHK_ON
for r_iid, _, is_sent in self._rows
)
if remaining:
self.win.after(0, lambda: self._import_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()
if _PIL_AVAILABLE:
try:
_base = Image.open(_ICON_PATH).convert("RGBA")
# multiple sizes → WM picks best for taskbar / alt-tab
self._wm_icons = [
ImageTk.PhotoImage(_base.resize((sz, sz), Image.LANCZOS))
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.session = None
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)
self._setup_tray()
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
if hasattr(self, "_tray") and _PIL_AVAILABLE:
try:
self._tray.icon = self._make_tray_image(color)
except Exception:
pass
# ── Pystray helpers ───────────────────────────────────────────────────────
def _make_tray_image(self, color="yellow"):
"""256×256 logo with a small colored status dot in the bottom-right."""
img = Image.open(_ICON_PATH).convert("RGBA").resize((256, 256), Image.LANCZOS)
dot_colors = {
"green": (0, 200, 0, 255),
"red": (220, 0, 0, 255),
"yellow": (230, 190, 0, 255),
}
c = dot_colors.get(color, (160, 160, 160, 255))
draw = ImageDraw.Draw(img)
r = 30
x0, y0 = 256 - 2*r - 8, 256 - 2*r - 8
draw.ellipse([x0, y0, x0 + 2*r, y0 + 2*r],
fill=c, outline=(0, 0, 0, 160), width=3)
return img
def _setup_tray(self):
if not _PYSTRAY_AVAILABLE or not _PIL_AVAILABLE:
return
menu = pystray.Menu(
pystray.MenuItem("GizerBridge", self._tray_show, default=True),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Manueller Import",
lambda: self._queue.put(self._open_manual_import)),
pystray.MenuItem("Einstellungen",
lambda: self._queue.put(self._open_config)),
pystray.MenuItem("Log",
lambda: self._queue.put(self._open_log)),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Beenden",
lambda: self._queue.put(self._quit)),
)
self._tray = pystray.Icon(
"gizerbridge",
self._make_tray_image("yellow"),
"GizerBridge",
menu,
)
self._tray.run_detached()
def _tray_show(self, *_):
self._queue.put(lambda: (self.root.deiconify(), self.root.lift()))
# ── 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:
_img = Image.open(_ICON_PATH).convert("RGBA")
_img = _img.resize((128, 128), Image.LANCZOS)
self._hdr_photo = ImageTk.PhotoImage(_img)
tk.Label(hdr, image=self._hdr_photo).pack(side=tk.LEFT, padx=(0, 8))
except Exception:
pass
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))
close_action = self.root.withdraw if _PYSTRAY_AVAILABLE else self._quit
self.root.protocol("WM_DELETE_WINDOW", close_action)
# ── 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
cookies, err = find_cookies()
if err:
self._log(f"Cookie-Fehler: {err}")
self._queue.put(lambda: self._set_icon_color("red"))
self._queue.put(lambda: messagebox.showwarning(
"Tralgo Cookie fehlt", err, parent=self.root))
return
self.session = build_session(cookies)
host = cfg.get("dtc_host", "localhost")
port = cfg.get("dtc_port", 11099)
accounts = cfg.get("accounts", [])
instruments = self._active_instruments(cfg)
self._log(f"Verbinde mit Sierra Chart DTC {host}:{port}")
def _on_trade(trade):
if trade["trade_id"] in self.sent:
return
try:
send_trade(self.session, trade, cfg["api_key"])
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"))
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.session,
self.config.get("api_key", ""),
self._on_trade_manually_imported,
)
def _on_trade_manually_imported(self, trade_id):
self.sent.add(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)
if hasattr(self, "_tray"):
try:
self._tray.stop()
except Exception:
pass
try:
self.root.quit()
self.root.destroy()
except tk.TclError:
pass