refactor: migrate to official Tralgo API, remove cookie/logfile parser
This commit is contained in:
@@ -14,7 +14,6 @@ DEFAULT_INSTRUMENTS = {
|
|||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
"email": "",
|
|
||||||
"dtc_host": "localhost",
|
"dtc_host": "localhost",
|
||||||
"dtc_port": 11099,
|
"dtc_port": 11099,
|
||||||
"accounts": [],
|
"accounts": [],
|
||||||
|
|||||||
@@ -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}
|
|
||||||
@@ -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()
|
|
||||||
-456
@@ -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("<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
|
|
||||||
+24
-39
@@ -1,55 +1,40 @@
|
|||||||
import requests
|
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 = {
|
_DIRECTION = {"Long": "B", "Short": "S", "B": "B", "S": "S"}
|
||||||
"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):
|
def _trade_payload(trade):
|
||||||
"""Create a requests.Session with tralgo.app cookies set."""
|
return {
|
||||||
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"],
|
"tradingaccount": trade["account"],
|
||||||
"instrument": trade["instrument"],
|
"instrument": trade["instrument"],
|
||||||
"type": trade["direction"],
|
"type": _DIRECTION.get(trade["direction"], trade["direction"]),
|
||||||
"lots": trade["lots"],
|
"lots": abs(trade["lots"]),
|
||||||
"entry_price": trade["entry_price"],
|
"entry_price": trade["entry_price"],
|
||||||
"exit_price": trade["exit_price"],
|
"exit_price": trade["exit_price"],
|
||||||
"entry_datetime": trade["entry_datetime"],
|
"entry_datetime": trade["entry_datetime"],
|
||||||
"exit_datetime": trade["exit_datetime"],
|
"exit_datetime": trade["exit_datetime"],
|
||||||
"fees": 0,
|
"fees": 0,
|
||||||
"pnl": trade["pnl"],
|
"transfer_token": trade["trade_id"],
|
||||||
"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)
|
|
||||||
|
|
||||||
|
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()
|
resp.raise_for_status()
|
||||||
try:
|
try:
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|||||||
+53
-45
@@ -7,9 +7,8 @@ from tkinter import messagebox, ttk
|
|||||||
|
|
||||||
from config import (DEFAULT_INSTRUMENTS, load_config, load_sent, save_config,
|
from config import (DEFAULT_INSTRUMENTS, load_config, load_sent, save_config,
|
||||||
save_sent)
|
save_sent)
|
||||||
from cookie_reader import find_cookies
|
|
||||||
from dtc_client import DTCClient, fills_to_trades, monitor_realtime
|
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:
|
try:
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -58,7 +57,6 @@ class ConfigWindow:
|
|||||||
pad = {"padx": 10, "pady": 3}
|
pad = {"padx": 10, "pady": 3}
|
||||||
|
|
||||||
self._api_key_var = self._labeled_entry("API Key *", cfg.get("api_key", ""), **pad)
|
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(
|
self._dtc_host_var = self._labeled_entry(
|
||||||
"SC DTC Host", cfg.get("dtc_host", "localhost"), **pad)
|
"SC DTC Host", cfg.get("dtc_host", "localhost"), **pad)
|
||||||
|
|
||||||
@@ -210,7 +208,6 @@ class ConfigWindow:
|
|||||||
}
|
}
|
||||||
self._on_save({
|
self._on_save({
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"email": self._email_var.get().strip(),
|
|
||||||
"dtc_host": self._dtc_host_var.get().strip() or "localhost",
|
"dtc_host": self._dtc_host_var.get().strip() or "localhost",
|
||||||
"dtc_port": dtc_port,
|
"dtc_port": dtc_port,
|
||||||
"accounts": list(self._acc_listbox.get(0, tk.END)),
|
"accounts": list(self._acc_listbox.get(0, tk.END)),
|
||||||
@@ -231,16 +228,16 @@ class ManualImportWindow:
|
|||||||
"Entry", "Exit", "PnL $", "Ticks", "Status")
|
"Entry", "Exit", "PnL $", "Ticks", "Status")
|
||||||
_WIDTHS = (30, 100, 140, 65, 70, 80, 80, 70, 55, 70)
|
_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 = tk.Toplevel(parent)
|
||||||
self.win.title("Manueller Import – GizerBridge")
|
self.win.title("Manueller Import – GizerBridge")
|
||||||
self.win.geometry("870x520")
|
self.win.geometry("870x520")
|
||||||
self.win.resizable(True, True)
|
self.win.resizable(True, True)
|
||||||
self._config = config
|
self._config = config
|
||||||
self._sent = sent # set of trade IDs already sent
|
self._sent = sent # set of trade IDs already sent
|
||||||
self._session = session # requests.Session or None
|
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._on_imported = on_imported # callback(trade_id) → updates App.sent
|
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._rows = [] # [(iid, trade_dict, is_sent)]
|
||||||
self._build()
|
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="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)
|
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 = tk.Label(bot, text="", fg="gray")
|
||||||
self._status_lbl.pack(side=tk.LEFT, padx=12)
|
self._status_lbl.pack(side=tk.LEFT, padx=12)
|
||||||
@@ -416,6 +416,7 @@ class ManualImportWindow:
|
|||||||
if already:
|
if already:
|
||||||
parts.append(f"{already} bereits importiert")
|
parts.append(f"{already} bereits importiert")
|
||||||
self._count_lbl.config(text=" – ".join(parts))
|
self._count_lbl.config(text=" – ".join(parts))
|
||||||
|
self._refresh_import_btn()
|
||||||
|
|
||||||
# ── Checkbox interaction ──────────────────────────────────────────────────
|
# ── Checkbox interaction ──────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -426,18 +427,22 @@ class ManualImportWindow:
|
|||||||
for row_iid, trade, is_sent in self._rows:
|
for row_iid, trade, is_sent in self._rows:
|
||||||
if row_iid != iid:
|
if row_iid != iid:
|
||||||
continue
|
continue
|
||||||
|
if is_sent:
|
||||||
|
return
|
||||||
vals = list(self._tree.item(iid, "values"))
|
vals = list(self._tree.item(iid, "values"))
|
||||||
if vals[0] == self._CHK_OFF:
|
if vals[0] == self._CHK_OFF:
|
||||||
vals[0] = self._CHK_ON
|
vals[0] = self._CHK_ON
|
||||||
self._tree.item(iid, values=vals, tags=("sel",))
|
self._tree.item(iid, values=vals, tags=("sel",))
|
||||||
else:
|
else:
|
||||||
vals[0] = self._CHK_OFF
|
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
|
break
|
||||||
self._refresh_import_btn()
|
self._refresh_import_btn()
|
||||||
|
|
||||||
def _select_all(self):
|
def _select_all(self):
|
||||||
for iid, _, is_sent in self._rows:
|
for iid, _, is_sent in self._rows:
|
||||||
|
if is_sent:
|
||||||
|
continue
|
||||||
vals = list(self._tree.item(iid, "values"))
|
vals = list(self._tree.item(iid, "values"))
|
||||||
vals[0] = self._CHK_ON
|
vals[0] = self._CHK_ON
|
||||||
self._tree.item(iid, values=vals, tags=("sel",))
|
self._tree.item(iid, values=vals, tags=("sel",))
|
||||||
@@ -456,19 +461,36 @@ class ManualImportWindow:
|
|||||||
for iid, _, _ in self._rows
|
for iid, _, _ in self._rows
|
||||||
)
|
)
|
||||||
self._import_btn.config(state=tk.NORMAL if any_checked else tk.DISABLED)
|
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 ────────────────────────────────────────────────────────────────
|
# ── Import ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _import_selected(self):
|
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 = [
|
to_send = [
|
||||||
(iid, trade)
|
(iid, trade)
|
||||||
for iid, trade, _ in self._rows
|
for iid, trade, _ in self._rows
|
||||||
@@ -483,28 +505,21 @@ class ManualImportWindow:
|
|||||||
self._status_lbl.config(text=f"Importiere {len(to_send)} Trade(s)…")
|
self._status_lbl.config(text=f"Importiere {len(to_send)} Trade(s)…")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
ok = err = 0
|
trades_list = [trade for _, trade in to_send]
|
||||||
for iid, trade in to_send:
|
|
||||||
try:
|
try:
|
||||||
send_trade(self._session, trade, self._api_key)
|
send_trades_bulk(self._api_key, trades_list)
|
||||||
|
for iid, trade in to_send:
|
||||||
self._on_imported(trade["trade_id"])
|
self._on_imported(trade["trade_id"])
|
||||||
ok += 1
|
|
||||||
self.win.after(0, lambda i=iid, t=trade: self._mark_ok(i, t))
|
self.win.after(0, lambda i=iid, t=trade: self._mark_ok(i, t))
|
||||||
|
summary = f"Fertig: {len(to_send)} importiert"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err += 1
|
msg = str(e)
|
||||||
self.win.after(0, lambda i=iid, msg=str(e): self._mark_err(i, msg))
|
for iid, _ in to_send:
|
||||||
summary = f"Fertig: {ok} importiert"
|
self.win.after(0, lambda i=iid, m=msg: self._mark_err(i, m))
|
||||||
if err:
|
summary = f"Fehler: {msg[:60]}"
|
||||||
summary += f", {err} Fehler"
|
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._status_lbl.config(text=summary))
|
||||||
self.win.after(0, lambda: self._search_btn.config(state=tk.NORMAL))
|
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()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
|
|
||||||
@@ -539,7 +554,6 @@ class App:
|
|||||||
pass
|
pass
|
||||||
self.config = load_config()
|
self.config = load_config()
|
||||||
self.sent = load_sent()
|
self.sent = load_sent()
|
||||||
self.session = None
|
|
||||||
self._status_color = "yellow"
|
self._status_color = "yellow"
|
||||||
self._status_msg = "Initialisierung..."
|
self._status_msg = "Initialisierung..."
|
||||||
self._log_entries = []
|
self._log_entries = []
|
||||||
@@ -631,16 +645,6 @@ class App:
|
|||||||
self._queue.put(lambda: self._set_icon_color("red"))
|
self._queue.put(lambda: self._set_icon_color("red"))
|
||||||
return
|
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")
|
host = cfg.get("dtc_host", "localhost")
|
||||||
port = cfg.get("dtc_port", 11099)
|
port = cfg.get("dtc_port", 11099)
|
||||||
accounts = cfg.get("accounts", [])
|
accounts = cfg.get("accounts", [])
|
||||||
@@ -650,7 +654,7 @@ class App:
|
|||||||
if trade["trade_id"] in self.sent:
|
if trade["trade_id"] in self.sent:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
send_trade(self.session, trade, cfg["api_key"])
|
send_trade(cfg["api_key"], trade)
|
||||||
self.sent.add(trade["trade_id"])
|
self.sent.add(trade["trade_id"])
|
||||||
save_sent(self.sent)
|
save_sent(self.sent)
|
||||||
msg = (f"Übertragen: {trade['instrument']} {trade['direction']} "
|
msg = (f"Übertragen: {trade['instrument']} {trade['direction']} "
|
||||||
@@ -773,15 +777,19 @@ class App:
|
|||||||
self.root,
|
self.root,
|
||||||
self.config,
|
self.config,
|
||||||
self.sent,
|
self.sent,
|
||||||
self.session,
|
|
||||||
self.config.get("api_key", ""),
|
self.config.get("api_key", ""),
|
||||||
self._on_trade_manually_imported,
|
self._on_trade_manually_imported,
|
||||||
|
self._on_trade_reset,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_trade_manually_imported(self, trade_id):
|
def _on_trade_manually_imported(self, trade_id):
|
||||||
self.sent.add(trade_id)
|
self.sent.add(trade_id)
|
||||||
save_sent(self.sent)
|
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):
|
def _on_config_saved(self, new_cfg):
|
||||||
self.config = new_cfg
|
self.config = new_cfg
|
||||||
save_config(new_cfg)
|
save_config(new_cfg)
|
||||||
|
|||||||
Reference in New Issue
Block a user