initial release: GizerBridge v1.0

This commit is contained in:
2026-05-22 12:27:27 +02:00
commit b2e4a8b46a
23 changed files with 2773 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
# GizerBridge Sierra Chart → Tralgo Analytics
Überträgt abgeschlossene Trades automatisch von Sierra Chart (Wine/Bottles/Lutris)
an Tralgo Analytics.
---
## Installation
### Schritt 1 Terminal öffnen
Rechtsklick auf den Desktop → **"Terminal öffnen"**
(oder im App-Menü nach "Terminal" suchen)
### Schritt 2 In den App-Ordner wechseln
```
cd ~/Downloads/gizerbridge
```
### Schritt 3 Installations-Script ausführen
```
bash install.sh
```
Das Script installiert alle benötigten Komponenten automatisch.
### Schritt 4 App starten
Im App-Menü (z. B. Activities) nach **"GizerBridge"** suchen und starten.
---
## Erster Start Einrichtung
Beim ersten Start öffnet sich automatisch das **Einstellungs-Fenster**:
| Feld | Was hier einzutragen ist |
|------|--------------------------|
| **API Key** | Der `transfer_token` aus deinem Tralgo-Konto |
| **Tralgo E-Mail** | Deine Login-E-Mail (nur zur Anzeige) |
| **SC DTC Host** | Normalerweise `localhost` |
| **SC DTC Port** | Normalerweise `11099` |
| **Konten** | Kontonamen genau wie in Sierra Chart (z. B. `4PRO-RQTPUT`, `Sim1.simulated`) |
| **Instrumente** | Gewünschte Instrumente aktivieren, Tick-Werte prüfen |
Nach dem Speichern startet die App sofort mit der Überwachung.
---
## Tralgo-Login (einmalig pro 30 Tage)
Damit die App Zugriff auf dein Konto hat, muss Firefox bei tralgo.app eingeloggt sein:
1. **Firefox öffnen** und `tralgo.app` aufrufen
2. Mit **E-Mail und Passwort** einloggen
3. Den **2FA-Code** aus der E-Mail eingeben
4. **"Für 30 Tage merken" aktivieren** ← Das ist wichtig!
Die App liest den Login-Cookie aus Firefox dein Passwort wird nirgends gespeichert oder übertragen.
---
## System-Tray Icon
Nach dem Start erscheint ein farbiger Status-Dot auf dem Tray-Icon:
- **Grün** App läuft, wartet auf neue Trades
- **Gelb** Initialisierung
- **Rot** Fehler (Cookie abgelaufen, Verbindungsproblem, o.ä.)
Rechtsklick auf das Icon → Öffnen · Manueller Import · Einstellungen · Log · Beenden
### Kein Tray-Icon sichtbar? (GNOME)
GNOME zeigt Tray-Icons nur mit der **AppIndicator Extension**:
1. Im Browser `extensions.gnome.org` öffnen
2. Nach **"AppIndicator and KStatusNotifierItem Support"** suchen
3. Extension installieren und aktivieren
4. GizerBridge neu starten
---
## Was passiert im Hintergrund?
- Die App verbindet sich via DTC-Protokoll direkt mit Sierra Chart (localhost:11099)
- Beim **ersten Start** werden bereits vorhandene Trades als "bekannt" markiert
und **nicht** übertragen (verhindert Duplikate)
- Nur **neue** Trades nach dem Start werden an Tralgo gesendet
- Bereits gesendete Trade-IDs werden in `~/.gizerbridge_sent.json` gespeichert
- Die Konfiguration liegt in `~/.gizerbridge_config.json` (nur für dich lesbar)
---
## Sicherheit
- **Kein Passwort** wird gespeichert oder übertragen
- Die Firefox-Cookie-Datenbank wird nur als temporäre Kopie gelesen (nie verändert)
- Die Config-Datei mit dem API Key hat Zugriffsrechte `600` (nur dein Benutzer)
---
## Fehlerbehebung
| Problem | Lösung |
|---------|--------|
| "Cookie fehlt" Popup | In Firefox bei tralgo.app neu einloggen, "Für 30 Tage merken" aktivieren |
| Rotes Icon, Übertragungsfehler | Internetverbindung prüfen; Status-Fenster für Details öffnen |
| Keine Trades erkannt | Kontonamen in den Einstellungen mit SC-Kontonamen abgleichen |
| DTC-Verbindungsfehler | Sierra Chart öffnen, DTC-Server auf Port 11099 aktivieren |
Log der letzten 50 Aktionen: Rechtsklick auf Tray-Icon → **"Log"**
---
*Ein Projekt von [GizerLabs](https://gizerlabs.com)*
+157
View File
@@ -0,0 +1,157 @@
# GizerBridge Sierra Chart → Tralgo Analytics
**Automatically transfers completed trades from Sierra Chart to Tralgo Analytics — on Linux.**
Sierra Chart runs on Linux via Wine/Bottles/Lutris. Tralgo Analytics has no official importer for this setup. GizerBridge fills that gap.
> Built and used daily in live ES/NQ futures trading.
---
## How it works
GizerBridge connects directly to Sierra Chart via the **DTC Protocol** (TCP on `localhost:11099`).
No log file polling. No manual export. Trades are detected and transferred in real time as they close.
```
Sierra Chart (DTC Server)
│ TCP · localhost:11099
GizerBridge
│ HTTPS · Tralgo Analytics API
Tralgo Analytics
```
- Already-sent trade IDs are stored locally → no duplicate uploads
- Manual import available for historical trades
- Runs as a system tray app (AppImage, no installation required)
---
## Requirements
- Linux (any distribution)
- Sierra Chart running via Wine / Bottles / Lutris
- DTC Protocol Server enabled in Sierra Chart
- Active Tralgo Analytics account
- Firefox (for session authentication)
### Enable DTC in Sierra Chart
`Global Settings → Data/Trade Service Settings → DTC Protocol Server`
→ Enable, Port: `11099`
---
## Installation
**Option A AppImage (recommended, no install needed)**
1. Download the latest `GizerBridge.AppImage` from [Releases](../../releases)
2. Make it executable:
```bash
chmod +x GizerBridge.AppImage
```
3. Run it:
```bash
./GizerBridge.AppImage
```
**Option B From source**
```bash
git clone https://github.com/gizerlabs/gizerbridge.git
cd gizerbridge
bash install.sh
```
---
## First Start Setup
On first launch, the settings window opens automatically:
| Field | What to enter |
|---|---|
| **API Key** | Your `transfer_token` from Tralgo Analytics |
| **Tralgo E-Mail** | Your login e-mail (display only) |
| **SC DTC Host** | `localhost` (default) |
| **SC DTC Port** | `11099` (default) |
| **Accounts** | Account names exactly as shown in Sierra Chart |
| **Instruments** | Enable instruments, verify tick sizes and values |
Save → the app connects immediately and starts monitoring.
---
## Tralgo Login (once every 30 days)
The app reads the session cookie from Firefox — your password is never stored or transmitted.
1. Open Firefox and go to `tralgo.app`
2. Log in with your e-mail and password
3. Enter the 2FA code from your e-mail
4. **Enable "Remember for 30 days"** ← required
---
## System Tray
After start, a colored status dot appears on the tray icon:
| Color | Status |
|---|---|
| 🟢 Green | Running, monitoring for new trades |
| 🟡 Yellow | Initializing |
| 🔴 Red | Error (session expired, connection issue) |
Right-click the tray icon → Open · Manual Import · Settings · Log · Quit
**No tray icon on GNOME?**
Install the [AppIndicator Extension](https://extensions.gnome.org/extension/615/appindicator-support/) and restart the app.
---
## Security
- No password is stored or transmitted
- Firefox cookie database is read as a temporary copy — never modified
- Config file (`~/.gizerbridge_config.json`) is stored with permissions `600`
- API key is stored locally, never logged or shared
---
## Troubleshooting
| Problem | Solution |
|---|---|
| "Cookie missing" popup | Log in to tralgo.app in Firefox, enable "Remember for 30 days" |
| Red icon / transfer error | Check internet connection; open Status window for details |
| No trades detected | Verify account names in settings match Sierra Chart exactly |
| DTC connection failed | Confirm DTC Protocol Server is enabled in Sierra Chart on port 11099 |
Last 50 events: Right-click tray icon → **Log**
---
## Built with
- Python 3.11
- [DTC Protocol](https://www.sierrachart.com/index.php?page=doc/DTCProtocol.html) (Sierra Chart)
- [Tralgo Analytics API](https://tralgo.app)
- `requests`, `Pillow`, `pystray`
---
## Status
Active development. Used daily in live trading on ES/NQ futures.
Feedback and issues welcome → open a [GitHub Issue](../../issues).
---
*Not affiliated with Sierra Chart or Tralgo Analytics.*
*A [GizerLabs](https://gizerlabs.com) project.*
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
import json
import os
import stat
CONFIG_PATH = os.path.expanduser("~/.gizerbridge_config.json")
SENT_PATH = os.path.expanduser("~/.gizerbridge_sent.json")
DEFAULT_INSTRUMENTS = {
"MNQ": {"tick_size": 0.25, "tick_value": 0.50, "enabled": True},
"NQ": {"tick_size": 0.25, "tick_value": 5.00, "enabled": False},
"ES": {"tick_size": 0.25, "tick_value": 12.50, "enabled": False},
"MES": {"tick_size": 0.25, "tick_value": 1.25, "enabled": False},
}
DEFAULT_CONFIG = {
"api_key": "",
"email": "",
"dtc_host": "localhost",
"dtc_port": 11099,
"accounts": [],
"instruments": DEFAULT_INSTRUMENTS,
}
def load_config():
if not os.path.exists(CONFIG_PATH):
return dict(DEFAULT_CONFIG)
with open(CONFIG_PATH, encoding="utf-8") as f:
cfg = json.load(f)
# Merge missing instrument defaults
for name, defaults in DEFAULT_INSTRUMENTS.items():
cfg.setdefault("instruments", {}).setdefault(name, defaults)
return cfg
def save_config(cfg):
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
os.chmod(CONFIG_PATH, stat.S_IRUSR | stat.S_IWUSR)
def load_sent():
if not os.path.exists(SENT_PATH):
return set()
with open(SENT_PATH, encoding="utf-8") as f:
return set(json.load(f))
def save_sent(sent_set):
with open(SENT_PATH, "w", encoding="utf-8") as f:
json.dump(list(sent_set), f)
+84
View File
@@ -0,0 +1,84 @@
import glob
import os
import shutil
import sqlite3
import tempfile
import time
FIREFOX_COOKIE_PATHS = [
"~/.mozilla/firefox/*/cookies.sqlite",
"~/snap/firefox/common/.mozilla/firefox/*/cookies.sqlite",
"~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/cookies.sqlite",
"~/.var/app/org.mozilla.firefox/config/mozilla/firefox/*/cookies.sqlite",
]
NO_COOKIE_MSG = (
"Bitte in Firefox bei tralgo.app einloggen und "
"'Für 30 Tage merken' aktivieren."
)
EXPIRED_MSG = (
"Deine tralgo.app Sitzung ist abgelaufen. "
"Bitte erneut in Firefox einloggen und 'Für 30 Tage merken' aktivieren."
)
def find_cookies():
"""Return (dict of cookie name→value, error_str).
On success error_str is None; on failure dict is None."""
all_dbs = []
for pattern in FIREFOX_COOKIE_PATHS:
all_dbs.extend(glob.glob(os.path.expanduser(pattern)))
if not all_dbs:
return None, NO_COOKIE_MSG
best_expiry = -1
best_cookies = None
for db_path in all_dbs:
tmp = None
try:
tmp = tempfile.mktemp(suffix="_tralgo_cookies.sqlite")
shutil.copy2(db_path, tmp)
cookies = _read_tralgo_cookies(tmp)
if not cookies:
continue
max_expiry = max(v["expiry"] for v in cookies.values())
if max_expiry > best_expiry:
best_expiry = max_expiry
best_cookies = cookies
except Exception:
pass
finally:
if tmp and os.path.exists(tmp):
try:
os.unlink(tmp)
except OSError:
pass
if best_cookies is None:
return None, NO_COOKIE_MSG
now = int(time.time())
if best_expiry < now:
return None, EXPIRED_MSG
return {name: data["value"] for name, data in best_cookies.items()}, None
def _read_tralgo_cookies(db_path):
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
try:
cur = conn.cursor()
cur.execute(
"SELECT name, value, expiry FROM moz_cookies "
"WHERE host LIKE '%tralgo.app%' "
"AND (name LIKE 'wordpress_logged_in%' OR name LIKE 'wp2fa_device%')"
)
rows = cur.fetchall()
finally:
conn.close()
if not rows:
return {}
return {row[0]: {"value": row[1], "expiry": row[2]} for row in rows}
+123
View File
@@ -0,0 +1,123 @@
"""Cookie debug tool — run with: python3 debug_cookies.py"""
import glob
import os
import shutil
import sqlite3
import tempfile
import time
FIREFOX_COOKIE_PATHS = [
"~/.mozilla/firefox/*/cookies.sqlite",
"~/snap/firefox/common/.mozilla/firefox/*/cookies.sqlite",
"~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/cookies.sqlite",
"~/.var/app/org.mozilla.firefox/config/mozilla/firefox/*/cookies.sqlite",
]
SEPARATOR = "-" * 70
def main():
print(SEPARATOR)
print("Firefox Cookie Debug")
print(SEPARATOR)
# ── 1. Find all DB files ──────────────────────────────────────────────────
all_dbs = []
for pattern in FIREFOX_COOKIE_PATHS:
expanded = os.path.expanduser(pattern)
matches = glob.glob(expanded)
print(f"\nPattern: {pattern}")
if matches:
for m in matches:
print(f" FOUND: {m}")
all_dbs.append(m)
else:
print(f" (keine Treffer)")
print(f"\n{SEPARATOR}")
print(f"Gesamt: {len(all_dbs)} cookies.sqlite Datei(en)")
print(SEPARATOR)
if not all_dbs:
print("\nKeine Firefox-Profile gefunden.")
print("Ist Firefox installiert? (Standard, Snap, oder Flatpak?)")
return
# ── 2. Read tralgo.app cookies from each DB ───────────────────────────────
now = int(time.time())
for db_path in all_dbs:
print(f"\nDB: {db_path}")
tmp = None
try:
tmp = tempfile.mktemp(suffix="_debug.sqlite")
shutil.copy2(db_path, tmp)
conn = sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)
try:
cur = conn.cursor()
# All tralgo.app cookies
cur.execute(
"SELECT name, value, expiry, host FROM moz_cookies "
"WHERE host LIKE '%tralgo%' ORDER BY name"
)
all_rows = cur.fetchall()
# Only auth cookies the app needs
cur.execute(
"SELECT name, value, expiry FROM moz_cookies "
"WHERE host LIKE '%tralgo.app%' "
"AND (name LIKE 'wordpress_logged_in%' "
" OR name LIKE 'wp2fa_device%')"
)
auth_rows = cur.fetchall()
finally:
conn.close()
if not all_rows:
print(" Keine tralgo.app Cookies gefunden.")
else:
print(f" Alle tralgo.app Cookies ({len(all_rows)} Stück):")
for name, value, expiry, host in all_rows:
expired = expiry < now
exp_str = (f"abgelaufen seit {now - expiry}s"
if expired else f"gültig noch {expiry - now}s")
print(f" [{exp_str:>30}] {name[:60]}")
if auth_rows:
print(f"\n Auth-Cookies für GizerBridge ({len(auth_rows)} Stück):")
for name, value, expiry in auth_rows:
expired = expiry < now
status = "ABGELAUFEN" if expired else "OK"
print(f" [{status}] {name[:60]}")
print(f" Wert: {value[:60]}...")
else:
print("\n FEHLER: Keine Auth-Cookies (wordpress_logged_in / wp2fa_device).")
print(" Lösung: Firefox öffnen → tralgo.app → einloggen →")
print(" 'Für 30 Tage merken' aktivieren.")
except Exception as e:
print(f" Fehler beim Lesen: {e}")
finally:
if tmp and os.path.exists(tmp):
try:
os.unlink(tmp)
except OSError:
pass
# ── 3. Final verdict ──────────────────────────────────────────────────────
print(f"\n{SEPARATOR}")
from cookie_reader import find_cookies
cookies, err = find_cookies()
if err:
print(f"Ergebnis: FEHLER — {err}")
else:
print(f"Ergebnis: OK — {len(cookies)} Auth-Cookie(s) bereit")
for name in cookies:
print(f" {name[:60]}")
print(SEPARATOR)
if __name__ == "__main__":
main()
+520
View File
@@ -0,0 +1,520 @@
"""DTC Binary protocol client for Sierra Chart (default: localhost:11099).
Binary wire format (no pragma pack → default C alignment):
Every message: [uint16 Size][uint16 Type][payload...]
Size = total byte count including the header.
Struct offsets used here match MSVC/GCC x64 default alignment:
- int64/double are 8-byte aligned (padding inserted before them when needed)
Supported messages:
Send: LOGON_REQUEST, HEARTBEAT, HISTORICAL_ORDER_FILLS_REQUEST,
OPEN_ORDERS_REQUEST
Receive: LOGON_RESPONSE, HEARTBEAT, HISTORICAL_ORDER_FILL_RESPONSE,
ORDER_UPDATE
"""
import re
import socket
import struct
import threading
import time
from datetime import datetime
# ── DTC message type constants ────────────────────────────────────────────────
_LOGON_REQUEST = 1
_LOGON_RESPONSE = 2
_HEARTBEAT = 3
_OPEN_ORDERS_REQ = 300
_ORDER_UPDATE = 301
_HIST_FILLS_REQ = 303
_HIST_FILL_RESP = 304
_PROTOCOL_VER = 8
_LOGON_SUCCESS = 1
_BUY = 1
_SELL = 2
# ── Encoding helpers ──────────────────────────────────────────────────────────
def _s(text, n):
"""Return exactly n bytes: ASCII-encoded, null-padded."""
b = (text or "").encode("ascii", errors="replace")[:n]
return b.ljust(n, b"\x00")
def _unpack_str(data, offset, n):
return data[offset:offset + n].rstrip(b"\x00").decode("ascii", errors="replace")
def _unpack_ts(data, offset):
"""Read int64 Unix timestamp at offset → datetime or None."""
raw = struct.unpack_from("<q", data, offset)[0]
try:
return datetime.fromtimestamp(raw) if 0 < raw < 4_102_444_800 else None
except (OSError, OverflowError):
return None
def _base_symbol(sc_symbol, instruments):
"""'MNQM6''MNQ' if 'MNQ' is in instruments, else None."""
m = re.search(r'([A-Z]{2,4})[A-Z]\d', sc_symbol)
if m and m.group(1) in instruments:
return m.group(1)
return sc_symbol if sc_symbol in instruments else None
# ── Message builders ──────────────────────────────────────────────────────────
# Format strings (little-endian, explicit padding to match C struct alignment)
_FMT_LOGON = "<HHi32s32s64siiii32s64s32si" # 284 bytes
_FMT_HEARTBEAT = "<HHIq" # 16 bytes
_FMT_HIST_FILLS = "<HHi32si32sB7xq" # 92 bytes (7x = padding before int64)
_FMT_OPEN_ORD = "<HHii" # 12 bytes
def _build_logon(heartbeat_secs=10):
size = struct.calcsize(_FMT_LOGON)
return struct.pack(
_FMT_LOGON,
size, _LOGON_REQUEST, _PROTOCOL_VER,
_s("", 32), _s("", 32), _s("GizerBridge", 64),
0, 0, heartbeat_secs, 0,
_s("", 32), _s("", 64), _s("gizerbridge", 32), 0,
)
def _build_heartbeat():
size = struct.calcsize(_FMT_HEARTBEAT)
return struct.pack(_FMT_HEARTBEAT, size, _HEARTBEAT, 0, int(time.time()))
def _build_hist_request(req_id, account, num_days):
size = struct.calcsize(_FMT_HIST_FILLS)
return struct.pack(
_FMT_HIST_FILLS,
size, _HIST_FILLS_REQ,
req_id,
_s("", 32), # ServerOrderID empty = all orders
num_days,
_s(account, 32), # TradeAccount
1, # RequestAllHistoricalFills = true
0, # StartDateTime = 0 (unused when num_days > 0)
)
def _build_open_orders_request():
size = struct.calcsize(_FMT_OPEN_ORD)
return struct.pack(_FMT_OPEN_ORD, size, _OPEN_ORDERS_REQ, 1, 0)
# ── Message parsers ───────────────────────────────────────────────────────────
# HISTORICAL_ORDER_FILL_RESPONSE empirisch ermittelte Offsets (480-Byte-Pakete):
# 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
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]
return {
"req_id": struct.unpack_from("<i", data, 4)[0],
"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 "",
}
except struct.error:
return None
# ORDER_UPDATE field offsets (C struct, default alignment):
# 0 uint16 Size
# 2 uint16 Type
# 4 int32 RequestID
# 8 int32 TotalNumMessages
# 12 int32 MessageNumber
# 16 char[64] Symbol
# 80 char[16] Exchange
# 96 char[32] PreviousServerOrderID
# 128 char[32] ServerOrderID
# 160 char[32] ClientOrderID
# 192 char[32] ExchangeOrderID
# 224 uint32 OrderStatus
# 228 uint32 OrderUpdateReason
# 232 uint32 OrderType
# 236 uint32 BuySell
# 240 double Price1 (240 % 8 == 0 ✓)
# 248 double Price2
# 256 uint32 TimeInForce
# 260 [4 bytes padding]
# 264 int64 GoodTillDateTime (264 % 8 == 0 ✓)
# 272 double OrderQuantity
# 280 double FilledQuantity
# 288 double RemainingQuantity
# 296 double AverageFillPrice
# 304 double LastFillPrice
# 312 int64 LastFillDateTime
# 320 double LastFillQuantity
# 328 char[64] UniqueExecutionID
# 392 char[32] TradeAccount
def _parse_order_update(data):
if len(data) < 328:
return None
try:
qty = struct.unpack_from("<d", data, 320)[0]
price = struct.unpack_from("<d", data, 304)[0]
if qty <= 0 or price <= 0:
return None # not a fill event
uid = _unpack_str(data, 328, 64) if len(data) >= 392 else ""
return {
"symbol": _unpack_str(data, 16, 64),
"buysell": struct.unpack_from("<I", data, 236)[0],
"price": price,
"qty": qty,
"datetime": _unpack_ts(data, 312),
"account": _unpack_str(data, 392, 32) if len(data) >= 424 else "",
"unique_id": uid,
}
except struct.error:
return None
# ── DTCClient ─────────────────────────────────────────────────────────────────
class DTCClient:
"""Low-level DTC Binary protocol client (not thread-safe for concurrent sends)."""
def __init__(self, host="localhost", port=11099):
self._host = host
self._port = port
self._sock = None
self._buf = b""
self._lock = threading.Lock()
def connect(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((self._host, self._port))
sock.settimeout(None)
self._sock = sock
def disconnect(self):
if self._sock:
try:
self._sock.close()
except OSError:
pass
self._sock = None
def logon(self, heartbeat_secs=10):
"""Send LOGON_REQUEST and block until LOGON_RESPONSE received."""
self._send(_build_logon(heartbeat_secs))
for msg_type, data in self._read_iter(timeout=10):
if msg_type == _LOGON_RESPONSE and len(data) >= 16:
result = struct.unpack_from("<i", data, 8)[0]
text = _unpack_str(data, 12, 96)
if result != _LOGON_SUCCESS:
raise ConnectionError(f"DTC Logon abgelehnt: {text}")
return
raise TimeoutError("Kein LOGON_RESPONSE erhalten (Timeout)")
def request_fills(self, account, num_days=30):
"""Request historical fills for account. Returns list of fill dicts."""
req_id = int(time.time() * 1000) & 0x7FFFFFFF
self._send(_build_hist_request(req_id, account, num_days))
fills = []
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:
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:
fills.append(f)
return fills
def subscribe_order_updates(self):
"""Send OPEN_ORDERS_REQUEST so SC starts pushing ORDER_UPDATE messages."""
self._send(_build_open_orders_request())
def send_heartbeat(self):
self._send(_build_heartbeat())
def read_messages(self, timeout=5):
"""Non-blocking read: return all complete messages buffered within timeout."""
self._sock.settimeout(timeout)
msgs = []
try:
chunk = self._sock.recv(65536)
if not chunk:
raise ConnectionError("DTC-Verbindung vom Server geschlossen")
self._buf += chunk
except socket.timeout:
pass
while len(self._buf) >= 4:
size = struct.unpack_from("<H", self._buf, 0)[0]
if size < 4 or len(self._buf) < size:
break
msg_type = struct.unpack_from("<H", self._buf, 2)[0]
msgs.append((msg_type, bytes(self._buf[:size])))
self._buf = self._buf[size:]
return msgs
# ── Internal ──────────────────────────────────────────────────────────────
def _send(self, data):
with self._lock:
self._sock.sendall(data)
def _read_iter(self, timeout):
"""Generator yielding (msg_type, data) tuples until timeout."""
deadline = time.time() + timeout
while time.time() < deadline:
remaining = max(0.1, deadline - time.time())
self._sock.settimeout(remaining)
try:
chunk = self._sock.recv(65536)
if not chunk:
raise ConnectionError("DTC-Verbindung geschlossen")
self._buf += chunk
except socket.timeout:
pass
while len(self._buf) >= 4:
size = struct.unpack_from("<H", self._buf, 0)[0]
if size < 4 or len(self._buf) < size:
break
msg_type = struct.unpack_from("<H", self._buf, 2)[0]
data = bytes(self._buf[:size])
self._buf = self._buf[size:]
yield msg_type, data
# ── Trade reconstruction from fills ──────────────────────────────────────────
def fills_to_trades(fills, instruments):
"""Convert list of fill dicts to completed round-trip trade dicts.
Groups fills by (account, base_symbol), sorts by datetime, then matches
buys and sells until the net position returns to zero that's one trade.
"""
groups = {}
for f in sorted(fills, key=lambda x: x.get("datetime") or datetime.min):
base = _base_symbol(f["symbol"], instruments)
if not base:
continue
groups.setdefault((f["account"], base), []).append(f)
trades = []
for (account, symbol), group in groups.items():
trades.extend(_match_fills(group, account, symbol, instruments))
return trades
def _match_fills(fills, account, symbol, instruments):
trades, pending, position = [], [], 0.0
for f in fills:
delta = f["qty"] if f["buysell"] == _BUY else -f["qty"]
position += delta
pending.append(f)
if abs(position) < 0.01:
t = _pending_to_trade(pending, account, symbol, instruments)
if t:
trades.append(t)
pending, position = [], 0.0
return trades
def _pending_to_trade(fills, account, symbol, instruments):
buys = [f for f in fills if f["buysell"] == _BUY]
sells = [f for f in fills if f["buysell"] == _SELL]
if not buys or not sells:
return None
direction = "Long" if fills[0]["buysell"] == _BUY else "Short"
entry_fs = buys if direction == "Long" else sells
exit_fs = sells if direction == "Long" else buys
e_qty = sum(f["qty"] for f in entry_fs)
x_qty = sum(f["qty"] for f in exit_fs)
ep = sum(f["price"] * f["qty"] for f in entry_fs) / e_qty
xp = sum(f["price"] * f["qty"] for f in exit_fs) / x_qty
lots = int(round(e_qty))
_fmt = "%Y-%m-%d %H:%M:%S"
entry_ts = min((f["datetime"] for f in entry_fs if f["datetime"]), default=None)
exit_ts = max((f["datetime"] for f in exit_fs if f["datetime"]), default=None)
entry_str = entry_ts.strftime(_fmt) if entry_ts else datetime.now().strftime(_fmt)
exit_str = exit_ts.strftime(_fmt) if exit_ts else datetime.now().strftime(_fmt)
cfg = instruments[symbol]
tick = cfg["tick_size"]
tick_val = cfg["tick_value"]
pnl_ticks = round((xp - ep if direction == "Long" else ep - xp) / tick) * lots
pnl = round(pnl_ticks * tick_val, 2)
if direction == "Long":
execs = f"Buy {lots} @ {ep:.4f} / {entry_str},Sell {lots} @ {xp:.4f} / {exit_str}"
else:
execs = f"Sell {lots} @ {ep:.4f} / {entry_str},Buy {lots} @ {xp:.4f} / {exit_str}"
return {
"trade_id": f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{entry_str}",
"instrument": symbol,
"direction": direction,
"lots": lots,
"entry_price": round(ep, 4),
"exit_price": round(xp, 4),
"entry_datetime": entry_str,
"exit_datetime": exit_str,
"pnl": pnl,
"pnl_ticks": pnl_ticks,
"executions": execs,
"account": account,
}
# ── Real-time monitor ─────────────────────────────────────────────────────────
def monitor_realtime(host, port, accounts, instruments, on_trade, stop_event, log_fn,
on_ready=None):
"""Connect to SC DTC server, load existing fills, then stream ORDER_UPDATE.
Calls on_trade(trade_dict) for each newly completed round-trip trade.
Calls on_ready(existing_ids) once the connection is established and the
historical fill scan is complete before entering the real-time loop.
Returns set of trade IDs that were already present at startup (caller marks
these as sent so they don't get re-uploaded after a restart).
Returns None if the connection fails immediately.
"""
client = DTCClient(host, port)
try:
client.connect()
client.logon(heartbeat_secs=10)
except Exception as e:
log_fn(f"DTC-Verbindungsfehler: {e}")
return None
# ── Scan existing fills so we know what's already been uploaded ────────────
existing_ids = set()
all_fills = []
for account in accounts:
try:
fills = client.request_fills(account, num_days=30)
all_fills.extend(fills)
except Exception as e:
log_fn(f"Historische Fills ({account}): {e}")
for t in fills_to_trades(all_fills, instruments):
existing_ids.add(t["trade_id"])
# ── Subscribe to real-time ORDER_UPDATE messages ──────────────────────────
try:
client.subscribe_order_updates()
except Exception as e:
log_fn(f"ORDER_UPDATE-Subscription fehlgeschlagen: {e}")
if on_ready:
on_ready(existing_ids)
# Per-(account, symbol) state for real-time fill accumulation
rt_pending = {} # (account, symbol) → [fill dicts]
rt_position = {} # (account, symbol) → float
seen_uids = set()
last_hb = time.time()
while not stop_event.is_set():
try:
msgs = client.read_messages(timeout=3)
except Exception as e:
log_fn(f"DTC Lesefehler: {e}")
break
if time.time() - last_hb >= 10:
try:
client.send_heartbeat()
except Exception:
pass
last_hb = time.time()
for msg_type, data in msgs:
if msg_type == _HEARTBEAT:
try:
client.send_heartbeat()
except Exception:
pass
elif msg_type == _ORDER_UPDATE:
f = _parse_order_update(data)
if not f:
continue
uid = f["unique_id"]
if uid and uid in seen_uids:
continue
if uid:
seen_uids.add(uid)
base = _base_symbol(f["symbol"], instruments)
if not base or not f["account"]:
continue
key = (f["account"], base)
rt_position.setdefault(key, 0.0)
rt_pending.setdefault(key, [])
rt_position[key] += f["qty"] if f["buysell"] == _BUY else -f["qty"]
rt_pending[key].append(f)
if abs(rt_position[key]) < 0.01:
group = rt_pending.pop(key)
rt_position[key] = 0.0
t = _pending_to_trade(group, f["account"], base, instruments)
if t and t["trade_id"] not in existing_ids:
existing_ids.add(t["trade_id"])
on_trade(t)
client.disconnect()
return existing_ids
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -e
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
EXEC="python3 $APP_DIR/main.py"
ICON_DIR="$HOME/.local/share/icons"
DESKTOP_DIR="$HOME/.local/share/applications"
ICON_PATH="$ICON_DIR/gizerbridge.png"
DESKTOP_FILE="$DESKTOP_DIR/gizerbridge.desktop"
export APP_DIR ICON_PATH
echo "=== GizerBridge Installation ==="
echo ""
# ── Python dependencies ────────────────────────────────────────────────────
echo "[1/5] Installiere Python-Abhängigkeiten..."
pip install -r "$APP_DIR/requirements.txt" --user --quiet
echo " Fertig."
# ── App icon ───────────────────────────────────────────────────────────────
echo "[2/5] Kopiere App-Icon..."
mkdir -p "$ICON_DIR"
cp "$APP_DIR/icon.png" "$ICON_PATH"
echo " Icon gespeichert: $ICON_PATH"
# ── .desktop file ──────────────────────────────────────────────────────────
echo "[3/5] Erstelle Desktop-Eintrag..."
mkdir -p "$DESKTOP_DIR"
cat > "$DESKTOP_FILE" <<DESKTOP
[Desktop Entry]
Name=GizerBridge
Comment=Sierra Chart → Tralgo Analytics
Exec=$EXEC
Icon=gizerbridge
Terminal=false
Type=Application
Categories=Office;Finance;
StartupNotify=false
DESKTOP
chmod +x "$DESKTOP_FILE"
echo " Gespeichert: $DESKTOP_FILE"
# ── Remove old desktop entries ─────────────────────────────────────────────
echo "[4/5] Entferne alte Desktop-Einträge..."
rm -f "$DESKTOP_DIR"/*tralgo*.desktop "$DESKTOP_DIR"/*analytics*.desktop 2>/dev/null || true
echo " Fertig."
# ── Update desktop database ────────────────────────────────────────────────
echo "[5/5] Aktualisiere App-Datenbank..."
if command -v update-desktop-database &>/dev/null; then
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
fi
if command -v gtk-update-icon-cache &>/dev/null; then
gtk-update-icon-cache -f "$ICON_DIR" 2>/dev/null || true
fi
echo ""
echo "✓ GizerBridge wurde installiert."
echo " Suche ihn im App-Menü unter 'GizerBridge'."
echo " Oder direkt starten: python3 $APP_DIR/main.py"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
import sys
import os
# Ensure the app directory is on the path regardless of how it's launched
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tray_app import App
def main():
app = App()
app.run()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
requests>=2.28.0
Pillow>=9.0.0
pystray>=0.19.0
+284
View File
@@ -0,0 +1,284 @@
"""Diagnose-Script für DTC HISTORICAL_ORDER_FILL_RESPONSE Paket-Struktur.
Startet SC, verbindet via DTC, empfängt Fill-Pakete und analysiert
die Byte-Struktur um den Preis-Offset zu finden.
Aufruf: python3 test_dtc_diag.py [account] [num_days]
"""
import socket
import struct
import sys
import time
from datetime import datetime
# ── DTC Konstanten ──────────────────────────────────────────────────────────
_LOGON_REQUEST = 1
_LOGON_RESPONSE = 2
_HEARTBEAT = 3
_HIST_FILLS_REQ = 303
_HIST_FILL_RESP = 304
_PROTOCOL_VER = 8
def _s(text, n):
b = (text or "").encode("ascii", errors="replace")[:n]
return b.ljust(n, b"\x00")
def _build_logon():
fmt = "<HHi32s32s64siiii32s64s32si"
size = struct.calcsize(fmt)
return struct.pack(fmt,
size, _LOGON_REQUEST, _PROTOCOL_VER,
_s("", 32), _s("", 32), _s("GizerBridge-Diag", 64),
0, 0, 10, 0, _s("", 32), _s("", 64), _s("diag", 32), 0)
def _build_hist_req(req_id, account, num_days):
fmt = "<HHi32si32sB7xq"
size = struct.calcsize(fmt)
return struct.pack(fmt,
size, _HIST_FILLS_REQ,
req_id, _s("", 32), num_days, _s(account, 32), 1, 0)
def _recv_msg(sock, buf):
while True:
if len(buf) >= 4:
size = struct.unpack_from("<H", buf, 0)[0]
if size >= 4 and len(buf) >= size:
msg = bytes(buf[:size])
return msg, buf[size:]
try:
chunk = sock.recv(65536)
if not chunk:
raise ConnectionError("Verbindung geschlossen")
buf += chunk
except socket.timeout:
return None, buf
def _unpack_str(data, offset, n):
return data[offset:offset+n].rstrip(b"\x00").decode("ascii", errors="replace")
def _ts(raw):
try:
return datetime.fromtimestamp(raw).strftime("%Y-%m-%d %H:%M:%S") if 0 < raw < 4_102_444_800 else f"invalid({raw})"
except Exception:
return f"error({raw})"
def hex_dump(data, label=""):
"""Formatierter Hex-Dump mit ASCII-Darstellung."""
print(f"\n{'='*60}")
if label:
print(f" {label}")
print(f" Länge: {len(data)} Bytes")
print(f"{'='*60}")
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
print(f" {i:4d} ({i:04X}): {hex_part:<48} |{ascii_part}|")
def analyze_fill_packet(data, fill_num):
"""Umfassende Analyse eines HIST_FILL_RESP Pakets."""
n = len(data)
print(f"\n{'#'*60}")
print(f" FILL-PAKET #{fill_num} ({n} Bytes)")
print(f"{'#'*60}")
# Bekannte Felder ausgeben
print("\n--- Bestätigte Felder ---")
print(f" [0] Size (uint16) = {struct.unpack_from('<H', data, 0)[0]}")
print(f" [2] Type (uint16) = {struct.unpack_from('<H', data, 2)[0]}")
print(f" [4] ReqID (int32) = {struct.unpack_from('<i', data, 4)[0]}")
if n >= 16:
print(f" [8] TotalMsg(int32) = {struct.unpack_from('<i', data, 8)[0]}")
print(f" [12] MsgNum (int32) = {struct.unpack_from('<i', data, 12)[0]}")
if n >= 80:
sym = _unpack_str(data, 16, 64)
print(f" [16] Symbol (ch[64]) = '{sym}'")
if n >= 96:
exch = _unpack_str(data, 80, 16)
print(f" [80] Exchange(ch[16]) = '{exch}'")
if n >= 132:
bs = struct.unpack_from("<I", data, 128)[0]
print(f" [128] BuySell (uint32) = {bs} ({'BUY' if bs==1 else 'SELL' if bs==2 else '???'})")
if n >= 152:
dt_raw = struct.unpack_from("<q", data, 144)[0]
print(f" [144] DateTime(int64) = {dt_raw} = {_ts(dt_raw)}")
if n >= 160:
qty = struct.unpack_from("<d", data, 152)[0]
print(f" [152] Volume (double) = {qty}")
if n >= 192:
tid = _unpack_str(data, 160, 32)
print(f" [160] TradeID (ch[32]) = '{tid}'")
if n >= 256:
acc = _unpack_str(data, 224, 32)
print(f" [224] Account (ch[32]) = '{acc}'")
# Unbekannte Zone 96-128 analysieren
print("\n--- Zone 96-128 (UNBEKANNT, möglicherweise Preis) ---")
for off in range(96, min(128, n-3), 4):
if off + 4 <= n:
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=".")
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)
print("\n--- Zone 128-160 (zwischen BuySell und DateTime/Volume) ---")
for off in range(128, min(160, n-3), 4):
if off + 4 <= n:
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=".")
label = ""
if off == 128: label = " ← BuySell (bestätigt)"
if off == 144: label = " ← DateTime (bestätigt)"
if off == 152: label = " ← Volume (bestätigt)"
print(f" [{off:3d}] uint32={as_uint32:10d} int32={as_int32:10d} float32={as_float32:12.4f} ascii='{as_ascii}'{label}")
# Doubles-Scan über gesamtes Paket
print("\n--- Doubles (alle 8-Byte-ausgerichteten Offsets) ---")
for off in range(0, n-7, 8):
val = struct.unpack_from("<d", data, off)[0]
tag = ""
if 1.0 <= val <= 10.0: tag = " ← Lots?"
if 5000 <= val <= 30000: tag = " ← PREIS? (Futures-Range)"
if 1_600_000_000 <= val <= 2_000_000_000: tag = " ← Timestamp als double?"
if tag or (off in (96, 104, 112, 120, 136, 152)):
print(f" [{off:3d}] double = {val:20.6f}{tag}")
# Alle uint32 die MNQ-Preis in Ticks sein könnten (80000-100000)
print("\n--- uint32 im Bereich 50000-120000 (Preis in Ticks à 0.25?) ---")
for off in range(0, n-3, 4):
val = struct.unpack_from("<I", data, off)[0]
if 50000 <= val <= 120000:
print(f" [{off:3d}] uint32 = {val} → Preis = {val * 0.25:.2f}")
# uint32 im Bereich für MNQ-Preis * 100
print("\n--- uint32 im Bereich 1500000-3000000 (Preis * 100?) ---")
for off in range(0, n-3, 4):
val = struct.unpack_from("<I", data, off)[0]
if 1_500_000 <= val <= 3_000_000:
print(f" [{off:3d}] uint32 = {val} → Preis/100 = {val / 100:.2f}")
# int64 Scan (könnte Preis als Festkomma sein)
print("\n--- int64 im Bereich 50000-5000000 (Preis-Integer?) ---")
for off in range(0, n-7, 8):
val = struct.unpack_from("<q", data, off)[0]
if 50_000 <= val <= 5_000_000:
print(f" [{off:3d}] int64 = {val} → /4={val/4:.2f} /100={val/100:.2f} /400={val/400:.4f}")
def main():
account = sys.argv[1] if len(sys.argv) > 1 else ""
num_days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
print(f"DTC Diagnose-Tool")
print(f"Account: '{account}', Tage: {num_days}")
print(f"Verbinde zu localhost:11099 ...")
try:
sock = socket.socket()
sock.settimeout(10)
sock.connect(("localhost", 11099))
sock.settimeout(5)
print("Verbunden!")
except Exception as e:
print(f"FEHLER: {e}")
return
buf = b""
# Logon
sock.sendall(_build_logon())
for _ in range(20):
msg, buf = _recv_msg(sock, buf)
if msg is None:
continue
mtype = struct.unpack_from("<H", msg, 2)[0]
if mtype == _LOGON_RESPONSE:
result = struct.unpack_from("<i", msg, 8)[0]
text = _unpack_str(msg, 12, 96)
print(f"Logon-Antwort: result={result}, text='{text}'")
if result != 1:
print("FEHLER: Logon abgelehnt")
return
break
# Historical fills request
req_id = int(time.time() * 1000) & 0x7FFFFFFF
sock.sendall(_build_hist_req(req_id, account, num_days))
print(f"\nHistorical-Fills-Anfrage gesendet (req_id={req_id})")
fill_count = 0
raw_packets = []
deadline = time.time() + 20
while time.time() < deadline:
msg, buf = _recv_msg(sock, buf)
if msg is None:
continue
mtype = struct.unpack_from("<H", msg, 2)[0]
if mtype != _HIST_FILL_RESP:
continue
fill_count += 1
raw_packets.append(msg)
# Zeige die ersten 3 und letzten 2 Pakete im Detail
if fill_count <= 3 or (fill_count <= 5 and len(msg) < 100):
analyze_fill_packet(msg, fill_count)
# Prüfe NoOrderFills an verschiedenen Offsets
for check_off in [256, 257, 260, 264, 188, 180]:
if len(msg) > check_off:
val = struct.unpack_from("<B", msg, check_off)[0]
if val == 1:
print(f"\n>>> NoOrderFills=1 gefunden bei Offset {check_off}! (fill_count={fill_count})")
# Letztes Paket immer anzeigen (NoOrderFills-Paket)
if fill_count == 1:
print(f"\n=== ERSTES PAKET (hex dump) ===")
hex_dump(msg)
print(f"\n\nGesamt empfangene Fill-Pakete: {fill_count}")
if raw_packets:
print("\n=== LETZTES PAKET (hex dump) ===")
hex_dump(raw_packets[-1], f"Paket #{fill_count}")
if len(raw_packets) >= 2:
print("\n=== VERGLEICH: Preis-relevante Bytes in allen Paketen ===")
print(f" {'Offset':<8} ", end="")
for i in range(min(len(raw_packets), 12)):
print(f"{'Paket'+str(i+1):>12}", end="")
print()
# Zeige uint32-Werte für Offsets 96-200
for off in range(88, 200, 4):
row = f" [{off:3d}] "
interesting = False
vals = []
for pkt in raw_packets[:12]:
if len(pkt) > off + 3:
v = struct.unpack_from("<I", pkt, off)[0]
vals.append(v)
row += f"{v:>12}"
else:
row += f"{'N/A':>12}"
# Interessant wenn Werte variieren (könnten Preise sein)
if vals and max(vals) != min(vals):
interesting = True
if off in (128, 144, 152, 160):
interesting = True
if interesting:
print(row + " ← variiert!" if max(vals) != min(vals) else row)
sock.close()
print("\nFertig.")
if __name__ == "__main__":
main()
+456
View File
@@ -0,0 +1,456 @@
"""Sierra Chart TradeActivityLog parser.
Binary .data files are decoded field-by-field (TLV format):
[uint32 LE: field_id] [uint32 LE: length] [bytes * length]
Field 0x66 → SCDateTime (int64 LE, microseconds since 1899-12-30) ← timestamp
Field 0x67 → symbol string
Field 0x68 → log message text
parse_binary_log() returns [(datetime_or_None, str)] for each log entry.
All internal parsers work on this list; timestamps propagate to _make_trade.
Sim accounts (contain 'sim'/'simulated' in account name):
- Entry: User order entry ... AOE=true (not a Flatten)
- Entry price: first 'Trade simulation fill ... Last: X' after signal
- Direction: 'Modifying Attached Order from parent fill. Parent base price: B.
New price: P' → Long if P > B
- Exit type 1 (TP/SL): next simulation fill at price ≠ entry price
- Exit type 2 (Flatten): 'Flatten&CancelAllOrders | Last: X. Current Position
quantity: N' → exit = X, lots = |N|
Live accounts:
- Entry: Updated Internal Position Quantity to N. Previous: 0
- Entry price: first 'Modifying Attached Order from parent fill.
Parent base price: X' after entry update
- Exit: Updated Internal Position Quantity to 0. Previous: N
- Exit price (priority order):
1. 'Flatten&CancelAllOrders | Last: X'
2. 'Rithmic Direct - DTC (Filled)' + 'Limit' nearby → tp_price
3. 'Rithmic Direct - DTC (Filled)' + 'Stop' nearby → sl_price
4. 'User order entry | Last: X | AOE=true' (Reverse entry)
"""
import glob
import os
import re
import struct
from datetime import datetime, timedelta
_LOG_GLOB = "TradeActivityLog_*_UTC.{account}.data"
# SCDateTime: int64 microseconds since 1899-12-30
_SC_EPOCH = datetime(1899, 12, 30)
_SC_TS_MIN = int((datetime(2000, 1, 1) - _SC_EPOCH).total_seconds() * 1_000_000)
_SC_TS_MAX = int((datetime(2060, 1, 1) - _SC_EPOCH).total_seconds() * 1_000_000)
_FIELD_DATETIME = 0x66 # SCDateTime
_FIELD_MESSAGE = 0x68 # log message text
# ── File discovery ─────────────────────────────────────────────────────────────
def find_sc_log_dir():
"""Return first detected SC TradeActivityLogs directory, or None."""
patterns = [
"~/.local/share/bottles/bottles/*/drive_c/SierraChart*/TradeActivityLogs/",
"~/Games/SierraChart*/TradeActivityLogs/",
"~/Games/sierra-chart*/TradeActivityLogs/",
"~/.wine/drive_c/SierraChart*/TradeActivityLogs/",
"~/.PlayOnLinux/wineprefix/*/drive_c/SierraChart*/TradeActivityLogs/",
]
for pattern in patterns:
matches = glob.glob(os.path.expanduser(pattern))
if matches:
return matches[0]
return None
def find_log_files(log_dir, accounts):
"""Return [(filepath, account_name)] for all matching log files."""
if not log_dir or not os.path.isdir(log_dir):
return []
result = []
for account in accounts:
pattern = os.path.join(log_dir, _LOG_GLOB.format(account=account))
for path in glob.glob(pattern):
result.append((path, account))
return result
# ── Binary log parser ──────────────────────────────────────────────────────────
def parse_binary_log(filepath):
"""Read binary SC .data file.
Returns [(datetime_or_None, message_str)] — one entry per log line,
timestamped with the SCDateTime field that preceded it in the file.
"""
try:
with open(filepath, "rb") as f:
data = f.read()
except OSError:
return []
entries = []
i = 0
n = len(data)
cur_ts = None
while i + 8 <= n:
try:
field_id, field_len = struct.unpack_from("<II", data, i)
except struct.error:
break
i += 8
if i + field_len > n:
break
value = data[i : i + field_len]
i += field_len
if field_id == _FIELD_DATETIME and field_len == 8:
raw, = struct.unpack("<q", value)
cur_ts = (_SC_EPOCH + timedelta(microseconds=raw)
if _SC_TS_MIN < raw < _SC_TS_MAX else None)
elif field_id == _FIELD_MESSAGE and field_len > 0:
text = value.rstrip(b"\x00").decode("ascii", errors="replace")
if text.strip():
entries.append((cur_ts, text))
return entries
extract_strings = parse_binary_log # alias used by tray_app
# ── Public entry point ─────────────────────────────────────────────────────────
def parse_trades(entries, account, instruments, trade_date=None):
"""Parse all complete trades from [(datetime_or_None, str)] entry list."""
if _is_sim(account):
return _parse_sim(entries, account, instruments, trade_date)
return _parse_live(entries, account, instruments, trade_date)
# ── Shared helpers ─────────────────────────────────────────────────────────────
def _is_sim(account):
low = account.lower()
return "sim" in low or "simulated" in low
def _text(entry):
"""Extract the string from a (ts, str) entry."""
return entry[1]
def _ts(entry):
"""Extract the datetime (or None) from a (ts, str) entry."""
return entry[0]
def _extract_instrument(s, known):
m = re.search(r'\b([A-Z]{2,4})[A-Z]\d', s)
if m and m.group(1) in known:
return m.group(1)
return None
_PRICE = r"(\d+(?:\.\d+)?)" # matches 29744.25 but not a trailing bare period
def _ts_str(ts, trade_date=None):
"""Format a datetime for the API: actual time → HH:MM:SS; fallback → 00:00:00."""
if ts:
return ts.strftime("%Y-%m-%d %H:%M:%S")
if trade_date:
return f"{trade_date} 00:00:00"
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def _find_direction(entries, start, n, window=40):
"""Long if first Modifying new_price > base, Short otherwise."""
for j in range(start, min(start + window, n)):
s = _text(entries[j])
if "Order modified internally" in s:
continue
m = re.search(
r"Modifying Attached Order from parent fill\."
r" Parent base price: " + _PRICE + r"\. New price: " + _PRICE, s)
if m:
return "Long" if float(m.group(2)) > float(m.group(1)) else "Short"
return None
def _nearby_order_type(entries, dtc_idx, n, window=4):
"""After DTC (Filled) at dtc_idx, look at nearby strings for order type."""
for k in range(dtc_idx + 1, min(dtc_idx + window, n)):
s = _text(entries[k])
if "Market" in s:
return "Market"
if "Limit" in s:
return "Limit"
if "Stop" in s:
return "Stop"
return None
def _make_trade(instrument, direction, lots, entry_price, exit_price,
account, instruments, entry_ts=None, exit_ts=None, trade_date=None):
cfg = instruments[instrument]
tick_size = cfg["tick_size"]
tick_value = cfg["tick_value"]
if direction == "Long":
pnl_ticks = round((exit_price - entry_price) / tick_size) * lots
else:
pnl_ticks = round((entry_price - exit_price) / tick_size) * lots
pnl = round(pnl_ticks * tick_value, 2)
entry_str = _ts_str(entry_ts, trade_date)
exit_str = _ts_str(exit_ts, trade_date)
if direction == "Long":
exec_str = (f"Buy {lots} @ {entry_price} / {entry_str},"
f"Sell {lots} @ {exit_price} / {exit_str}")
else:
exec_str = (f"Sell {lots} @ {entry_price} / {entry_str},"
f"Buy {lots} @ {exit_price} / {exit_str}")
return {
"trade_id": f"{account}_{instrument}_{entry_price}_{exit_price}",
"instrument": instrument,
"direction": direction,
"lots": lots,
"entry_price": entry_price,
"exit_price": exit_price,
"entry_datetime": entry_str,
"exit_datetime": exit_str,
"pnl": pnl,
"pnl_ticks": pnl_ticks,
"executions": exec_str,
"account": account,
}
# ── Sim parser ─────────────────────────────────────────────────────────────────
def _parse_sim(entries, account, instruments, trade_date=None):
trades = []
current_inst = None
i = 0
n = len(entries)
while i < n:
s = _text(entries[i])
inst = _extract_instrument(s, instruments)
if inst:
current_inst = inst
if (re.search(r"User order entry.+AOE=true", s)
and "Flatten" not in s
and current_inst):
entry_idx, entry_price = _sim_next_fill(entries, i + 1, n)
if entry_idx is None:
i += 1
continue
direction = _find_direction(entries, i + 1, n)
exit_idx, exit_price, lots, exit_direction = _sim_find_exit(
entries, entry_idx + 1, n, entry_price)
if exit_idx is None:
i += 1
continue
if direction is None:
direction = exit_direction
if direction is None:
i += 1
continue
if lots is None:
lots = 1
entry_ts = _ts(entries[entry_idx])
exit_ts = _ts(entries[exit_idx])
trades.append(_make_trade(
current_inst, direction, lots,
entry_price, exit_price, account, instruments,
entry_ts=entry_ts, exit_ts=exit_ts, trade_date=trade_date))
i = exit_idx + 1
continue
i += 1
return trades
def _sim_next_fill(entries, start, n, window=60):
"""Return (idx, last_price) of next simulation fill."""
for j in range(start, min(start + window, n)):
m = re.search(
r"simulation fill\. Bid: \S+ Ask: \S+ Last: " + _PRICE,
_text(entries[j]))
if m:
return j, float(m.group(1))
return None, None
def _sim_find_exit(entries, start, n, entry_price, window=400):
"""Find Sim trade exit. Returns (exit_idx, exit_price, lots, direction_or_None)."""
for j in range(start, min(start + window, n)):
s = _text(entries[j])
m_flat = re.search(
r"Flatten&CancelAllOrders \| Last: " + _PRICE
+ r".*?Current Position quantity: (-?\d+)", s)
if m_flat:
qty = int(m_flat.group(2))
direction = "Long" if qty > 0 else "Short"
return j, float(m_flat.group(1)), abs(qty), direction
m_fill = re.search(
r"simulation fill\. Bid: \S+ Ask: \S+ Last: " + _PRICE, s)
if m_fill:
price = float(m_fill.group(1))
if abs(price - entry_price) > 0.1:
fill_type = _nearby_order_type(entries, j, n)
if fill_type == "Limit":
direction = "Long" if price > entry_price else "Short"
elif fill_type == "Stop":
direction = "Long" if price < entry_price else "Short"
else:
direction = None
return j, price, None, direction
return None, None, None, None
# ── Live parser ────────────────────────────────────────────────────────────────
def _parse_live(entries, account, instruments, trade_date=None):
trades = []
current_inst = None
last_signal_price = None
i = 0
n = len(entries)
while i < n:
s = _text(entries[i])
inst = _extract_instrument(s, instruments)
if inst:
current_inst = inst
m_sig = re.search(
r"User order entry \| Last: " + _PRICE + r" \| AOE=true", s)
if m_sig and "Flatten" not in s:
last_signal_price = float(m_sig.group(1))
m_entry = re.search(
r"Updated Internal Position Quantity to (-?\d+)\. Previous: 0", s)
if m_entry and current_inst:
new_qty = int(m_entry.group(1))
if new_qty == 0:
i += 1
continue
direction = "Long" if new_qty > 0 else "Short"
lots = abs(new_qty)
entry_ts = _ts(entries[i])
entry_price, tp_price, sl_price = _live_bracket_prices(entries, i + 1, n)
if entry_price is None:
entry_price = last_signal_price
if entry_price is None:
i += 1
continue
exit_idx = _live_exit_idx(entries, i + 1, n)
if exit_idx is None:
i += 1
continue
exit_price = _live_exit_price(entries, i + 1, exit_idx, tp_price, sl_price, n)
if exit_price is None:
i += 1
continue
exit_ts = _ts(entries[exit_idx])
last_signal_price = None
trades.append(_make_trade(
current_inst, direction, lots,
entry_price, exit_price, account, instruments,
entry_ts=entry_ts, exit_ts=exit_ts, trade_date=trade_date))
i = exit_idx + 1
continue
i += 1
return trades
def _live_bracket_prices(entries, start, n, window=40):
"""Return (entry_price, tp_price, sl_price) from bracket modification strings."""
entry_price = None
bracket_new_prices = []
for j in range(start, min(start + window, n)):
s = _text(entries[j])
if "Order modified internally" in s:
continue
m = re.search(
r"Modifying Attached Order from parent fill\."
r" Parent base price: " + _PRICE + r"\. New price: " + _PRICE, s)
if m:
if entry_price is None:
entry_price = float(m.group(1))
bracket_new_prices.append(float(m.group(2)))
if len(bracket_new_prices) >= 2:
break
tp_price = bracket_new_prices[0] if len(bracket_new_prices) >= 1 else None
sl_price = bracket_new_prices[1] if len(bracket_new_prices) >= 2 else None
return entry_price, tp_price, sl_price
def _live_exit_idx(entries, start, n):
"""Find index of position returning to 0."""
for j in range(start, n):
if re.search(
r"Updated Internal Position Quantity to 0\. Previous: -?\d+",
_text(entries[j])):
return j
return None
def _live_exit_price(entries, start, exit_idx, tp_price, sl_price, n):
"""Determine exit price from events between entry and exit position updates."""
for j in range(start, exit_idx):
m = re.search(r"Flatten&CancelAllOrders \| Last: " + _PRICE,
_text(entries[j]))
if m:
return float(m.group(1))
for j in range(start, exit_idx):
if "Rithmic Direct - DTC (Filled)" in _text(entries[j]):
otype = _nearby_order_type(entries, j, exit_idx)
if otype == "Limit" and tp_price is not None:
return tp_price
if otype == "Stop" and sl_price is not None:
return sl_price
for j in range(start, exit_idx):
s = _text(entries[j])
m = re.search(r"User order entry \| Last: " + _PRICE + r" \| AOE=true", s)
if m and "Flatten" not in s:
return float(m.group(1))
return None
+57
View File
@@ -0,0 +1,57 @@
import requests
TRALGO_URL = "https://tralgo.app/wp-admin/admin-ajax.php"
_HEADERS = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://tralgo.app",
"Referer": "https://tralgo.app/analytics-logbuch/",
}
def build_session(cookies):
"""Create a requests.Session with tralgo.app cookies set."""
session = requests.Session()
for name, value in cookies.items():
session.cookies.set(name, value, domain="tralgo.app")
return session
def send_trade(session, trade, api_key):
"""POST trade to Tralgo. Raises on HTTP error. Returns parsed JSON."""
payload = {
"action": "insert_new_trade",
"tradingaccount": trade["account"],
"instrument": trade["instrument"],
"type": trade["direction"],
"lots": trade["lots"],
"entry_price": trade["entry_price"],
"exit_price": trade["exit_price"],
"entry_datetime": trade["entry_datetime"],
"exit_datetime": trade["exit_datetime"],
"fees": 0,
"pnl": trade["pnl"],
"net_pnl": trade["pnl"],
"pnl_ticks": trade["pnl_ticks"],
"tp_price": 0, "sl_price": 0,
"tp_ticks": 0, "tp_usd": 0,
"sl_ticks": 0, "sl_usd": 0,
"setup": "",
"entry_specifications": "",
"exit_specifications": "",
"mistakes": "",
"market_conditions": "",
"path": "",
"trade_rating": 0,
"review_status": "Unreviewed",
"journal": "",
"executions": trade["executions"],
"transfer_token": api_key,
}
resp = session.post(TRALGO_URL, data=payload, headers=_HEADERS, timeout=15)
resp.raise_for_status()
try:
return resp.json()
except Exception:
return resp.text
+842
View File
@@ -0,0 +1,842 @@
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, ImageDraw, 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")
# ── 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 = []
error_msg = 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:
error_msg = str(e)
if error_msg:
self.win.after(0, lambda: self._search_done([], error_msg))
return
# Filter by date range
filtered = []
for f in all_fills:
if not f.get("datetime"):
filtered.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)
trades = fills_to_trades(filtered, instruments)
self.win.after(0, lambda: self._search_done(trades, None))
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()
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))
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)
self._setup_tray()
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
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 ───────────────────────────────────────────────────────────
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:
_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))
except Exception:
pass
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))
close_action = self.root.withdraw if _PYSTRAY_AVAILABLE else self._quit
self.root.protocol("WM_DELETE_WINDOW", close_action)
# ── 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)
self._log(f"Verbinde mit Sierra Chart DTC {host}:{port}")
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"))
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)
if hasattr(self, "_tray"):
try:
self._tray.stop()
except Exception:
pass
try:
self.root.quit()
self.root.destroy()
except tk.TclError:
pass