Files
gizerbridge/tray_app.py
T

801 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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._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 = []
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))
# ── 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()
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.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)
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
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)
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"))
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.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)
try:
self.root.quit()
self.root.destroy()
except tk.TclError:
pass