809 lines
34 KiB
Python
809 lines
34 KiB
Python
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("<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,
|
||
"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("<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._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
|