initial release: GizerBridge v1.0
This commit is contained in:
+842
@@ -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
|
||||
Reference in New Issue
Block a user