add CHANGELOG, icon_full, update gitignore
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.AppImage
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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
@@ -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("<B", data, 256)[0]) if len(data) > 256 else False
|
||||
price = struct.unpack_from("<d", data, 136)[0]
|
||||
qty = struct.unpack_from("<d", data, 152)[0]
|
||||
buysell = struct.unpack_from("<I", data, 128)[0]
|
||||
raw_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]
|
||||
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 {
|
||||
"req_id": struct.unpack_from("<i", data, 4)[0],
|
||||
"total": total,
|
||||
"msgno": msgno,
|
||||
"symbol": _unpack_str(data, 16, 64),
|
||||
"price": price,
|
||||
"qty": qty,
|
||||
"datetime": _unpack_ts(data, 144),
|
||||
"buysell": buysell,
|
||||
"account": _unpack_str(data, 224, 32),
|
||||
"no_fills": no_fills,
|
||||
"unique_id": _unpack_str(data, 353, 64) if len(data) >= 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("<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:
|
||||
return None # not a fill event
|
||||
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):
|
||||
if msg_type != _HIST_FILL_RESP:
|
||||
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)
|
||||
if not f:
|
||||
if not f or f["req_id"] != req_id:
|
||||
continue
|
||||
if f["no_fills"] or (total > 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,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
+2
-2
@@ -120,7 +120,7 @@ def analyze_fill_packet(data, fill_num):
|
||||
as_uint32 = 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_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}'")
|
||||
|
||||
# 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_int32 = struct.unpack_from("<i", 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 = ""
|
||||
if off == 128: label = " ← BuySell (bestätigt)"
|
||||
if off == 144: label = " ← DateTime (bestätigt)"
|
||||
|
||||
+45
-87
@@ -12,18 +12,28 @@ 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
|
||||
from PIL import Image, 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")
|
||||
_ICON_FULL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon_full.png")
|
||||
|
||||
_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 ─────────────────────────────────────────────────────────────
|
||||
@@ -335,7 +345,8 @@ class ManualImportWindow:
|
||||
|
||||
def _run():
|
||||
all_fills = []
|
||||
error_msg = None
|
||||
dtc_error = None
|
||||
|
||||
try:
|
||||
client = DTCClient(host, port)
|
||||
client.connect()
|
||||
@@ -345,27 +356,27 @@ class ManualImportWindow:
|
||||
all_fills.extend(fills)
|
||||
client.disconnect()
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
dtc_error = str(e)
|
||||
|
||||
if error_msg:
|
||||
self.win.after(0, lambda: self._search_done([], error_msg))
|
||||
if dtc_error and not all_fills:
|
||||
self.win.after(0, lambda: self._search_done([], dtc_error))
|
||||
return
|
||||
|
||||
# Filter by date range
|
||||
filtered = []
|
||||
filtered_fills = []
|
||||
for f in all_fills:
|
||||
if not f.get("datetime"):
|
||||
filtered.append(f)
|
||||
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.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()
|
||||
|
||||
Reference in New Issue
Block a user