diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c7cbe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +*.AppImage +__pycache__/ +*.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e968e43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog – GizerBridge + +A development history of GizerBridge, from first prototype to stable release. +Documented to show the real problems solved along the way. + +--- + +## v1.0.0 – First Stable Release (2026-05-22) + +### Phase 1 – First Prototype: HTTP Script +- Built initial Python script sending trades via HTTP POST to Tralgo Analytics +- **Problem:** Session cookie missing → HTTP 200 response but nothing appeared in Tralgo +- **Fix:** Manually copied cookie from Firefox → worked +- **Problem:** Cookie expired frequently → required manual copying every session +- **Attempted:** Auto-login with username/password → failed due to 2FA + redirect loop + +### Phase 2 – Automatic Cookie Reader +- Implemented automatic cookie extraction from Firefox SQLite database +- **Problem:** Firefox installed as Flatpak stores profile under non-standard path + (`~/.var/app/org.mozilla.firefox/...` instead of `~/.mozilla/...`) +- **Fix:** Added Flatpak path as fourth search location → resolved + +### Phase 3 – GUI Application +- Built full tkinter GUI with system tray, settings window, and manual import +- **Problem:** Tray icon showed as grey square on KDE/Wayland +- **Fix:** Removed pystray dependency, switched to tkinter-only main window +- **Problem:** Date filter failed to detect new log files +- **Root cause:** Bug in filename parser +- **Problem:** Trades imported with wrong date (today's date instead of original) +- **Root cause:** Used `datetime.now()` instead of parsing date from filename +- **Problem:** Times always showed `00:00:00` for historical trades +- **Root cause:** Sierra Chart log files contain no time information → accepted as limitation + +### Phase 4 – DTC Protocol Integration +- Pascal (Tralgo support) recommended DTC Protocol over log file parsing +- Sierra Chart DTC server already active on port 11099 +- Replaced log file parser with direct TCP connection via DTC Binary Protocol +- **Problem:** Prices transmitted as `raw_value / 100` → showed as `100000.00` in Tralgo +- **Root cause:** Division by 100 missing in auto-monitor function +- **Problem:** Trades sent twice +- **Root cause:** Two app instances running simultaneously +- **Fix:** Enforced single instance + +### Phase 5 – Simulation Account Support +- **Problem:** DTC returned empty packets for simulation accounts +- **Attempted fix:** Reactivated log file parser as hybrid fallback for sim accounts +- **Retested:** DTC does deliver sim fills correctly after all → log file parser removed again +- **Result:** DTC-only for all account types (live + sim) → stable + +### Phase 6 – Rebrand to GizerBridge +- Renamed app from "Analytics Importer" to **GizerBridge** +- Integrated GizerLabs logo (taskbar icon + header in main window) +- Built distributable Linux AppImage (no installation required) +- Added pystray back with correct KDE/Wayland configuration + +--- + +## Current Status + +| Feature | Status | +|---|---| +| Live account trade transfer | ✅ Working | +| Simulation account trade transfer | ✅ Working | +| Real prices and timestamps | ✅ Working | +| Auto-import (real-time) | ✅ Working | +| Manual import (historical trades) | ✅ Working | +| Linux AppImage | ✅ Built and tested | +| GizerLabs branding | ✅ Integrated | diff --git a/dtc_client.py b/dtc_client.py index e4aa041..8e851c8 100644 --- a/dtc_client.py +++ b/dtc_client.py @@ -112,46 +112,50 @@ def _build_open_orders_request(): # ── Message parsers ─────────────────────────────────────────────────────────── -# HISTORICAL_ORDER_FILL_RESPONSE – empirisch ermittelte Offsets (480-Byte-Pakete): +# HISTORICAL_ORDER_FILL_RESPONSE – Offsets empirisch bestätigt (480-Byte-Pakete, Rithmic): # 0 uint16 Size # 2 uint16 Type # 4 int32 RequestID # 8 int32 TotalNumberMessages # 12 int32 MessageNumber -# 16 char[64] Symbol -# 80 char[16] Exchange -# 96 char[32] ??? (ASCII-Feld, Inhalt noch unbekannt) -# 128 uint32 BuySell (1=BUY, 2=SELL) -# 132 uint32 ??? (4 Bytes Padding/unbekannt) -# 136 double Price (136 % 8 == 0, 8-Byte-aligned) -# 144 int64 DateTime (Unix-Sekunden) -# 152 double Volume/Qty -# 160 char[32] FillIdentifier/TradeID -# 192 char[32] ??? -# 224 char[32] TradeAccount -# 256 uint8 NoOrderFills -# 257 char[96] InfoText -# 353 char[64] UniqueExecutionID -# 417 char[32] ExchangeOrderID +# 16 char[64] Symbol (z.B. "MNQM6.CME") +# 80 char[16] Exchange (leer bei Rithmic, Exchange steckt im Symbol) +# 96 char[32] FillIdentifier (laufende Fill-Nr. als String, z.B. "592") +# 128 uint32 BuySell (1=BUY, 2=SELL) +# 132 uint32 unbekannt (immer 0) +# 136 double Price × 100 (z.B. 2902225.0 → 29022.25 Punkte) +# 144 int64 DateTime (Unix-Sekunden) +# 152 double Volume/Qty (Anzahl Kontrakte, z.B. 1.0) +# 160 char[32] TradeID (Rithmic Order-ID, z.B. "1031001") +# 192 char[32] unbekannt +# 224 char[32] TradeAccount (z.B. "4PRO-RQTPUT") +# 256 uint8 FillStatus (1=partial, 2=final – KEIN NoOrderFills-Flag) +# 257 char[96] InfoText (z.B. "Rithmic Direct - DTC (Filled)[final]") +# 353 char[64] unbekannt +# 416 char[64] UniqueExecutionID (z.B. "2537652419") def _parse_fill_resp(data): if len(data) < 260: return None try: - no_fills = bool(struct.unpack_from(" 256 else False - price = struct.unpack_from("= 417 else "", + "no_fills": total > 0 and msgno >= total, + "unique_id": _unpack_str(data, 416, 64) if len(data) >= 480 else "", } except struct.error: return None @@ -193,7 +197,7 @@ def _parse_order_update(data): return None try: qty = struct.unpack_from("= 392 else "" @@ -258,22 +262,14 @@ class DTCClient: for msg_type, data in self._read_iter(timeout=15): if msg_type != _HIST_FILL_RESP: continue - if len(data) < 16: - continue - if struct.unpack_from(" 0 and msgno >= total): - break - # Gültige Fills: BuySell muss 1 (BUY) oder 2 (SELL) sein, Qty > 0 - if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0: + if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0 and f["price"] > 0: fills.append(f) + if f["no_fills"]: + break return fills def subscribe_order_updates(self): @@ -400,8 +396,14 @@ def _pending_to_trade(fills, account, symbol, instruments): else: execs = f"Sell {lots} @ {ep:.4f} / {entry_str},Buy {lots} @ {xp:.4f} / {exit_str}" + # Trade-ID uses only actual fill datetimes — never datetime.now(). + # This makes IDs identical whether generated from the historical-fill path + # or from the real-time ORDER_UPDATE path for the same trade. + _id_ts = entry_ts or exit_ts + _id_str = _id_ts.strftime(_fmt) if _id_ts else "" + return { - "trade_id": f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{entry_str}", + "trade_id": f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{_id_str}", "instrument": symbol, "direction": direction, "lots": lots, diff --git a/icon.png b/icon.png index 2a2a967..f1ab076 100644 Binary files a/icon.png and b/icon.png differ diff --git a/icon_full.png b/icon_full.png new file mode 100644 index 0000000..cd6749e Binary files /dev/null and b/icon_full.png differ diff --git a/test_dtc_diag.py b/test_dtc_diag.py index 88efaff..39f54e8 100644 --- a/test_dtc_diag.py +++ b/test_dtc_diag.py @@ -120,7 +120,7 @@ def analyze_fill_packet(data, fill_num): as_uint32 = struct.unpack_from(" date_to: continue - filtered.append(f) + filtered_fills.append(f) - trades = fills_to_trades(filtered, instruments) - self.win.after(0, lambda: self._search_done(trades, None)) + 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() @@ -515,12 +526,12 @@ 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: - _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)) + ImageTk.PhotoImage(_load_icon(_ICON_PATH, sz)) for sz in (256, 128, 64, 48, 32) ] self.root.iconphoto(True, *self._wm_icons) @@ -541,7 +552,6 @@ class App: 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( @@ -554,56 +564,6 @@ class App: 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 ─────────────────────────────────────────────────────────── @@ -621,14 +581,14 @@ class App: 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)) + self._hdr_photo = ImageTk.PhotoImage(_load_icon(_ICON_FULL_PATH, 128)) + tk.Label(hdr, image=self._hdr_photo).pack(side=tk.LEFT) except Exception: - pass - tk.Label(hdr, text="GizerBridge", - font=("", 13, "bold")).pack(side=tk.LEFT) + 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) @@ -652,8 +612,7 @@ class App: 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) + self.root.protocol("WM_DELETE_WINDOW", self._quit) # ── Monitor thread ──────────────────────────────────────────────────────── @@ -687,8 +646,6 @@ class App: 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 @@ -708,6 +665,12 @@ class App: 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) @@ -830,11 +793,6 @@ class App: 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()