initial release: GizerBridge v1.0
This commit is contained in:
+118
@@ -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)*
|
||||||
@@ -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.
@@ -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)
|
||||||
@@ -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}
|
||||||
@@ -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
@@ -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
|
||||||
+61
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
requests>=2.28.0
|
||||||
|
Pillow>=9.0.0
|
||||||
|
pystray>=0.19.0
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
Reference in New Issue
Block a user