"""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(" 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(" 256 else False price = struct.unpack_from("= 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("= 392 else "" return { "symbol": _unpack_str(data, 16, 64), "buysell": struct.unpack_from("= 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(" 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("= 4: size = struct.unpack_from("= 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