add CHANGELOG, icon_full, update gitignore

This commit is contained in:
2026-05-22 18:42:25 +02:00
parent 408f36135d
commit c474b82bf0
7 changed files with 158 additions and 125 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.AppImage
__pycache__/
*.pyc
+68
View File
@@ -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 |
+38 -36
View File
@@ -112,46 +112,50 @@ def _build_open_orders_request():
# ── Message parsers ─────────────────────────────────────────────────────────── # ── 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 # 0 uint16 Size
# 2 uint16 Type # 2 uint16 Type
# 4 int32 RequestID # 4 int32 RequestID
# 8 int32 TotalNumberMessages # 8 int32 TotalNumberMessages
# 12 int32 MessageNumber # 12 int32 MessageNumber
# 16 char[64] Symbol # 16 char[64] Symbol (z.B. "MNQM6.CME")
# 80 char[16] Exchange # 80 char[16] Exchange (leer bei Rithmic, Exchange steckt im Symbol)
# 96 char[32] ??? (ASCII-Feld, Inhalt noch unbekannt) # 96 char[32] FillIdentifier (laufende Fill-Nr. als String, z.B. "592")
# 128 uint32 BuySell (1=BUY, 2=SELL) # 128 uint32 BuySell (1=BUY, 2=SELL)
# 132 uint32 ??? (4 Bytes Padding/unbekannt) # 132 uint32 unbekannt (immer 0)
# 136 double Price (136 % 8 == 0, 8-Byte-aligned) # 136 double Price × 100 (z.B. 2902225.0 → 29022.25 Punkte)
# 144 int64 DateTime (Unix-Sekunden) # 144 int64 DateTime (Unix-Sekunden)
# 152 double Volume/Qty # 152 double Volume/Qty (Anzahl Kontrakte, z.B. 1.0)
# 160 char[32] FillIdentifier/TradeID # 160 char[32] TradeID (Rithmic Order-ID, z.B. "1031001")
# 192 char[32] ??? # 192 char[32] unbekannt
# 224 char[32] TradeAccount # 224 char[32] TradeAccount (z.B. "4PRO-RQTPUT")
# 256 uint8 NoOrderFills # 256 uint8 FillStatus (1=partial, 2=final KEIN NoOrderFills-Flag)
# 257 char[96] InfoText # 257 char[96] InfoText (z.B. "Rithmic Direct - DTC (Filled)[final]")
# 353 char[64] UniqueExecutionID # 353 char[64] unbekannt
# 417 char[32] ExchangeOrderID # 416 char[64] UniqueExecutionID (z.B. "2537652419")
def _parse_fill_resp(data): def _parse_fill_resp(data):
if len(data) < 260: if len(data) < 260:
return None return None
try: try:
no_fills = bool(struct.unpack_from("<B", data, 256)[0]) if len(data) > 256 else False raw_price = struct.unpack_from("<d", data, 136)[0]
price = struct.unpack_from("<d", data, 136)[0] price = raw_price / 100.0 # Rithmic liefert price × 100
qty = struct.unpack_from("<d", data, 152)[0] qty = struct.unpack_from("<d", data, 152)[0]
buysell = struct.unpack_from("<I", data, 128)[0] buysell = struct.unpack_from("<I", data, 128)[0]
total = struct.unpack_from("<i", data, 8)[0]
msgno = struct.unpack_from("<i", data, 12)[0]
return { return {
"req_id": struct.unpack_from("<i", data, 4)[0], "req_id": struct.unpack_from("<i", data, 4)[0],
"total": total,
"msgno": msgno,
"symbol": _unpack_str(data, 16, 64), "symbol": _unpack_str(data, 16, 64),
"price": price, "price": price,
"qty": qty, "qty": qty,
"datetime": _unpack_ts(data, 144), "datetime": _unpack_ts(data, 144),
"buysell": buysell, "buysell": buysell,
"account": _unpack_str(data, 224, 32), "account": _unpack_str(data, 224, 32),
"no_fills": no_fills, "no_fills": total > 0 and msgno >= total,
"unique_id": _unpack_str(data, 353, 64) if len(data) >= 417 else "", "unique_id": _unpack_str(data, 416, 64) if len(data) >= 480 else "",
} }
except struct.error: except struct.error:
return None return None
@@ -193,7 +197,7 @@ def _parse_order_update(data):
return None return None
try: try:
qty = struct.unpack_from("<d", data, 320)[0] qty = struct.unpack_from("<d", data, 320)[0]
price = struct.unpack_from("<d", data, 304)[0] price = struct.unpack_from("<d", data, 304)[0] / 100.0
if qty <= 0 or price <= 0: if qty <= 0 or price <= 0:
return None # not a fill event return None # not a fill event
uid = _unpack_str(data, 328, 64) if len(data) >= 392 else "" uid = _unpack_str(data, 328, 64) if len(data) >= 392 else ""
@@ -258,22 +262,14 @@ class DTCClient:
for msg_type, data in self._read_iter(timeout=15): for msg_type, data in self._read_iter(timeout=15):
if msg_type != _HIST_FILL_RESP: if msg_type != _HIST_FILL_RESP:
continue continue
if len(data) < 16:
continue
if struct.unpack_from("<i", data, 4)[0] != req_id:
continue
total = struct.unpack_from("<i", data, 8)[0]
msgno = struct.unpack_from("<i", data, 12)[0]
f = _parse_fill_resp(data) f = _parse_fill_resp(data)
if not f: if not f or f["req_id"] != req_id:
continue continue
if f["no_fills"] or (total > 0 and msgno >= total): if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0 and f["price"] > 0:
break
# Gültige Fills: BuySell muss 1 (BUY) oder 2 (SELL) sein, Qty > 0
if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0:
fills.append(f) fills.append(f)
if f["no_fills"]:
break
return fills return fills
def subscribe_order_updates(self): def subscribe_order_updates(self):
@@ -400,8 +396,14 @@ def _pending_to_trade(fills, account, symbol, instruments):
else: else:
execs = f"Sell {lots} @ {ep:.4f} / {entry_str},Buy {lots} @ {xp:.4f} / {exit_str}" 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 { 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, "instrument": symbol,
"direction": direction, "direction": direction,
"lots": lots, "lots": lots,
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+2 -2
View File
@@ -120,7 +120,7 @@ def analyze_fill_packet(data, fill_num):
as_uint32 = struct.unpack_from("<I", data, off)[0] as_uint32 = struct.unpack_from("<I", data, off)[0]
as_int32 = struct.unpack_from("<i", data, off)[0] as_int32 = struct.unpack_from("<i", data, off)[0]
as_float32 = struct.unpack_from("<f", data, off)[0] as_float32 = struct.unpack_from("<f", data, off)[0]
as_ascii = data[off:off+4].decode("ascii", errors=".") as_ascii = data[off:off+4].decode("ascii", errors="replace")
print(f" [{off:3d}] uint32={as_uint32:10d} int32={as_int32:10d} float32={as_float32:12.4f} ascii='{as_ascii}'") print(f" [{off:3d}] uint32={as_uint32:10d} int32={as_int32:10d} float32={as_float32:12.4f} ascii='{as_ascii}'")
# Zone 128-160 analysieren (zwischen BuySell und TradeID) # Zone 128-160 analysieren (zwischen BuySell und TradeID)
@@ -130,7 +130,7 @@ def analyze_fill_packet(data, fill_num):
as_uint32 = struct.unpack_from("<I", data, off)[0] as_uint32 = struct.unpack_from("<I", data, off)[0]
as_int32 = struct.unpack_from("<i", data, off)[0] as_int32 = struct.unpack_from("<i", data, off)[0]
as_float32 = struct.unpack_from("<f", data, off)[0] as_float32 = struct.unpack_from("<f", data, off)[0]
as_ascii = data[off:off+4].decode("ascii", errors=".") as_ascii = data[off:off+4].decode("ascii", errors="replace")
label = "" label = ""
if off == 128: label = " ← BuySell (bestätigt)" if off == 128: label = " ← BuySell (bestätigt)"
if off == 144: label = " ← DateTime (bestätigt)" if off == 144: label = " ← DateTime (bestätigt)"
+45 -87
View File
@@ -12,18 +12,28 @@ from dtc_client import DTCClient, fills_to_trades, monitor_realtime
from tralgo_api import build_session, send_trade from tralgo_api import build_session, send_trade
try: try:
from PIL import Image, ImageDraw, ImageTk from PIL import Image, ImageTk
_PIL_AVAILABLE = True _PIL_AVAILABLE = True
except ImportError: except ImportError:
_PIL_AVAILABLE = False _PIL_AVAILABLE = False
try: _ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.png")
import pystray _ICON_FULL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon_full.png")
_PYSTRAY_AVAILABLE = True
except ImportError:
_PYSTRAY_AVAILABLE = False
_ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.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 ───────────────────────────────────────────────────────────── # ── Config window ─────────────────────────────────────────────────────────────
@@ -335,7 +345,8 @@ class ManualImportWindow:
def _run(): def _run():
all_fills = [] all_fills = []
error_msg = None dtc_error = None
try: try:
client = DTCClient(host, port) client = DTCClient(host, port)
client.connect() client.connect()
@@ -345,27 +356,27 @@ class ManualImportWindow:
all_fills.extend(fills) all_fills.extend(fills)
client.disconnect() client.disconnect()
except Exception as e: except Exception as e:
error_msg = str(e) dtc_error = str(e)
if error_msg: if dtc_error and not all_fills:
self.win.after(0, lambda: self._search_done([], error_msg)) self.win.after(0, lambda: self._search_done([], dtc_error))
return return
# Filter by date range filtered_fills = []
filtered = []
for f in all_fills: for f in all_fills:
if not f.get("datetime"): if not f.get("datetime"):
filtered.append(f) filtered_fills.append(f)
continue continue
d = f["datetime"].strftime("%Y-%m-%d") d = f["datetime"].strftime("%Y-%m-%d")
if date_from and d < date_from: if date_from and d < date_from:
continue continue
if date_to and d > date_to: if date_to and d > date_to:
continue continue
filtered.append(f) filtered_fills.append(f)
trades = fills_to_trades(filtered, instruments) all_trades = fills_to_trades(filtered_fills, instruments)
self.win.after(0, lambda: self._search_done(trades, None)) 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() threading.Thread(target=_run, daemon=True).start()
@@ -515,12 +526,12 @@ class App:
def __init__(self): def __init__(self):
self.root = tk.Tk() self.root = tk.Tk()
self.root.withdraw() self.root.withdraw()
self.root.title("GizerBridge")
self.root.tk.call("wm", "iconname", self.root, "gizerbridge")
if _PIL_AVAILABLE: if _PIL_AVAILABLE:
try: try:
_base = Image.open(_ICON_PATH).convert("RGBA")
# multiple sizes → WM picks best for taskbar / alt-tab
self._wm_icons = [ 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) for sz in (256, 128, 64, 48, 32)
] ]
self.root.iconphoto(True, *self._wm_icons) self.root.iconphoto(True, *self._wm_icons)
@@ -541,7 +552,6 @@ class App:
def run(self): def run(self):
self.root.after(200, self._process_queue) self.root.after(200, self._process_queue)
self._setup_tray()
if not self.config.get("api_key"): if not self.config.get("api_key"):
self.root.after(100, lambda: ConfigWindow( self.root.after(100, lambda: ConfigWindow(
@@ -554,56 +564,6 @@ class App:
def _set_icon_color(self, color): def _set_icon_color(self, color):
self._status_color = 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 ─────────────────────────────────────────────────────────── # ── Main window ───────────────────────────────────────────────────────────
@@ -621,14 +581,14 @@ class App:
hdr.pack(fill=tk.X) hdr.pack(fill=tk.X)
if _PIL_AVAILABLE: if _PIL_AVAILABLE:
try: try:
_img = Image.open(_ICON_PATH).convert("RGBA") self._hdr_photo = ImageTk.PhotoImage(_load_icon(_ICON_FULL_PATH, 128))
_img = _img.resize((128, 128), Image.LANCZOS) tk.Label(hdr, image=self._hdr_photo).pack(side=tk.LEFT)
self._hdr_photo = ImageTk.PhotoImage(_img)
tk.Label(hdr, image=self._hdr_photo).pack(side=tk.LEFT, padx=(0, 8))
except Exception: except Exception:
pass tk.Label(hdr, text="GizerBridge",
tk.Label(hdr, text="GizerBridge", font=("", 13, "bold")).pack(side=tk.LEFT)
font=("", 13, "bold")).pack(side=tk.LEFT) else:
tk.Label(hdr, text="GizerBridge",
font=("", 13, "bold")).pack(side=tk.LEFT)
# ── Status row ── # ── Status row ──
sf = tk.Frame(self.root, padx=14) sf = tk.Frame(self.root, padx=14)
@@ -652,8 +612,7 @@ class App:
tk.Button(bf, text=text, width=15, tk.Button(bf, text=text, width=15,
command=cmd).pack(side=tk.LEFT, padx=(0, 6)) 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", self._quit)
self.root.protocol("WM_DELETE_WINDOW", close_action)
# ── Monitor thread ──────────────────────────────────────────────────────── # ── Monitor thread ────────────────────────────────────────────────────────
@@ -687,8 +646,6 @@ class App:
accounts = cfg.get("accounts", []) accounts = cfg.get("accounts", [])
instruments = self._active_instruments(cfg) instruments = self._active_instruments(cfg)
self._log(f"Verbinde mit Sierra Chart DTC {host}:{port}")
def _on_trade(trade): def _on_trade(trade):
if trade["trade_id"] in self.sent: if trade["trade_id"] in self.sent:
return return
@@ -708,6 +665,12 @@ class App:
self._update_status_msg(err_msg) self._update_status_msg(err_msg)
self._queue.put(lambda: self._set_icon_color("red")) 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): def _on_ready(existing_ids):
self.sent.update(existing_ids) self.sent.update(existing_ids)
save_sent(self.sent) save_sent(self.sent)
@@ -830,11 +793,6 @@ class App:
def _quit(self): def _quit(self):
self._stop.set() self._stop.set()
save_sent(self.sent) save_sent(self.sent)
if hasattr(self, "_tray"):
try:
self._tray.stop()
except Exception:
pass
try: try:
self.root.quit() self.root.quit()
self.root.destroy() self.root.destroy()