From 3e55f5f12d06e32c8d4ec8f3b1b6c494bb4eecdf Mon Sep 17 00:00:00 2001 From: IvanGizer Date: Tue, 2 Jun 2026 19:36:14 +0200 Subject: [PATCH] refactor: migrate to official Tralgo API, remove cookie/logfile parser --- config.py | 1 - cookie_reader.py | 84 --------- debug_cookies.py | 123 ------------- trade_parser.py | 456 ----------------------------------------------- tralgo_api.py | 77 ++++---- tray_app.py | 102 ++++++----- 6 files changed, 86 insertions(+), 757 deletions(-) delete mode 100644 cookie_reader.py delete mode 100644 debug_cookies.py delete mode 100644 trade_parser.py diff --git a/config.py b/config.py index 1b5b335..3694e70 100644 --- a/config.py +++ b/config.py @@ -14,7 +14,6 @@ DEFAULT_INSTRUMENTS = { DEFAULT_CONFIG = { "api_key": "", - "email": "", "dtc_host": "localhost", "dtc_port": 11099, "accounts": [], diff --git a/cookie_reader.py b/cookie_reader.py deleted file mode 100644 index ba00d6b..0000000 --- a/cookie_reader.py +++ /dev/null @@ -1,84 +0,0 @@ -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} diff --git a/debug_cookies.py b/debug_cookies.py deleted file mode 100644 index 2283c35..0000000 --- a/debug_cookies.py +++ /dev/null @@ -1,123 +0,0 @@ -"""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() diff --git a/trade_parser.py b/trade_parser.py deleted file mode 100644 index 114c0e5..0000000 --- a/trade_parser.py +++ /dev/null @@ -1,456 +0,0 @@ -"""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(" n: - break - - value = data[i : i + field_len] - i += field_len - - if field_id == _FIELD_DATETIME and field_len == 8: - raw, = struct.unpack(" 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 diff --git a/tralgo_api.py b/tralgo_api.py index fa4b5ad..c90ddcf 100644 --- a/tralgo_api.py +++ b/tralgo_api.py @@ -1,55 +1,40 @@ import requests -TRALGO_URL = "https://tralgo.app/wp-admin/admin-ajax.php" +TRALGO_URL = "https://tralgo.app/wp-json/trade-api/v1/add-trade" -_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/", -} +_DIRECTION = {"Long": "B", "Short": "S", "B": "B", "S": "S"} -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, +def _trade_payload(trade): + return { + "tradingaccount": trade["account"], + "instrument": trade["instrument"], + "type": _DIRECTION.get(trade["direction"], trade["direction"]), + "lots": abs(trade["lots"]), + "entry_price": trade["entry_price"], + "exit_price": trade["exit_price"], + "entry_datetime": trade["entry_datetime"], + "exit_datetime": trade["exit_datetime"], + "fees": 0, + "transfer_token": trade["trade_id"], } - resp = session.post(TRALGO_URL, data=payload, headers=_HEADERS, timeout=15) + + +def send_trade(api_key, trade): + """POST a single trade. Raises on HTTP error.""" + return send_trades_bulk(api_key, [trade]) + + +def send_trades_bulk(api_key, trades): + """POST one or more trades in a single request. Raises on HTTP error.""" + if not trades: + return + headers = {"API-Key": api_key, "Content-Type": "application/json"} + if len(trades) == 1: + body = _trade_payload(trades[0]) + else: + body = {"trades": [_trade_payload(t) for t in trades]} + resp = requests.post(TRALGO_URL, json=body, headers=headers, timeout=15) resp.raise_for_status() try: return resp.json() diff --git a/tray_app.py b/tray_app.py index bfd2474..1c628c4 100644 --- a/tray_app.py +++ b/tray_app.py @@ -7,9 +7,8 @@ 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 +from tralgo_api import send_trade, send_trades_bulk try: from PIL import Image, ImageTk @@ -58,7 +57,6 @@ class ConfigWindow: 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) @@ -210,7 +208,6 @@ class ConfigWindow: } 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)), @@ -231,16 +228,16 @@ class ManualImportWindow: "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): + def __init__(self, parent, config, sent, api_key, on_imported, on_reset): 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._on_reset = on_reset # callback(trade_id) → removes from App.sent self._rows = [] # [(iid, trade_dict, is_sent)] self._build() @@ -312,6 +309,9 @@ class ManualImportWindow: 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._reset_btn = tk.Button(bot, text="Import zurücksetzen", + command=self._reset_sent, state=tk.DISABLED) + self._reset_btn.pack(side=tk.LEFT, padx=4) self._status_lbl = tk.Label(bot, text="", fg="gray") self._status_lbl.pack(side=tk.LEFT, padx=12) @@ -416,6 +416,7 @@ class ManualImportWindow: if already: parts.append(f"{already} bereits importiert") self._count_lbl.config(text=" – ".join(parts)) + self._refresh_import_btn() # ── Checkbox interaction ────────────────────────────────────────────────── @@ -426,18 +427,22 @@ class ManualImportWindow: for row_iid, trade, is_sent in self._rows: if row_iid != iid: continue + if is_sent: + return 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",)) + self._tree.item(iid, values=vals, tags=("desel",)) break self._refresh_import_btn() def _select_all(self): for iid, _, is_sent in self._rows: + if is_sent: + continue vals = list(self._tree.item(iid, "values")) vals[0] = self._CHK_ON self._tree.item(iid, values=vals, tags=("sel",)) @@ -456,19 +461,36 @@ class ManualImportWindow: for iid, _, _ in self._rows ) self._import_btn.config(state=tk.NORMAL if any_checked else tk.DISABLED) + any_sent = any(is_sent for _, _, is_sent in self._rows) + self._reset_btn.config(state=tk.NORMAL if any_sent else tk.DISABLED) + + # ── Reset sent ──────────────────────────────────────────────────────────── + + def _reset_sent(self): + if not messagebox.askyesno( + "Import zurücksetzen", + "Bereits importierte Trades zurücksetzen?\n" + "Sie können danach erneut importiert werden.", + parent=self.win, + ): + return + updated = [] + for iid, trade, is_sent in self._rows: + if is_sent: + self._on_reset(trade["trade_id"]) + vals = list(self._tree.item(iid, "values")) + vals[0] = self._CHK_OFF + vals[9] = "" + self._tree.item(iid, values=vals, tags=("desel",)) + updated.append((iid, trade, False)) + else: + updated.append((iid, trade, is_sent)) + self._rows = updated + self._refresh_import_btn() # ── 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 @@ -483,28 +505,21 @@ class ManualImportWindow: 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) + trades_list = [trade for _, trade in to_send] + try: + send_trades_bulk(self._api_key, trades_list) + for iid, trade in to_send: 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" + summary = f"Fertig: {len(to_send)} importiert" + except Exception as e: + msg = str(e) + for iid, _ in to_send: + self.win.after(0, lambda i=iid, m=msg: self._mark_err(i, m)) + summary = f"Fehler: {msg[:60]}" + self.win.after(0, lambda: self._import_btn.config(state=tk.NORMAL)) 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() @@ -539,7 +554,6 @@ class App: pass self.config = load_config() self.sent = load_sent() - self.session = None self._status_color = "yellow" self._status_msg = "Initialisierung..." self._log_entries = [] @@ -631,16 +645,6 @@ class App: 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", []) @@ -650,7 +654,7 @@ class App: if trade["trade_id"] in self.sent: return try: - send_trade(self.session, trade, cfg["api_key"]) + send_trade(cfg["api_key"], trade) self.sent.add(trade["trade_id"]) save_sent(self.sent) msg = (f"Übertragen: {trade['instrument']} {trade['direction']} " @@ -773,15 +777,19 @@ class App: self.root, self.config, self.sent, - self.session, self.config.get("api_key", ""), self._on_trade_manually_imported, + self._on_trade_reset, ) def _on_trade_manually_imported(self, trade_id): self.sent.add(trade_id) save_sent(self.sent) + def _on_trade_reset(self, trade_id): + self.sent.discard(trade_id) + save_sent(self.sent) + def _on_config_saved(self, new_cfg): self.config = new_cfg save_config(new_cfg)