add CHANGELOG, icon_full, update gitignore
This commit is contained in:
+38
-36
@@ -112,46 +112,50 @@ def _build_open_orders_request():
|
||||
|
||||
# ── Message parsers ───────────────────────────────────────────────────────────
|
||||
|
||||
# HISTORICAL_ORDER_FILL_RESPONSE – empirisch ermittelte Offsets (480-Byte-Pakete):
|
||||
# HISTORICAL_ORDER_FILL_RESPONSE – Offsets empirisch bestätigt (480-Byte-Pakete, Rithmic):
|
||||
# 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
|
||||
# 16 char[64] Symbol (z.B. "MNQM6.CME")
|
||||
# 80 char[16] Exchange (leer bei Rithmic, Exchange steckt im Symbol)
|
||||
# 96 char[32] FillIdentifier (laufende Fill-Nr. als String, z.B. "592")
|
||||
# 128 uint32 BuySell (1=BUY, 2=SELL)
|
||||
# 132 uint32 unbekannt (immer 0)
|
||||
# 136 double Price × 100 (z.B. 2902225.0 → 29022.25 Punkte)
|
||||
# 144 int64 DateTime (Unix-Sekunden)
|
||||
# 152 double Volume/Qty (Anzahl Kontrakte, z.B. 1.0)
|
||||
# 160 char[32] TradeID (Rithmic Order-ID, z.B. "1031001")
|
||||
# 192 char[32] unbekannt
|
||||
# 224 char[32] TradeAccount (z.B. "4PRO-RQTPUT")
|
||||
# 256 uint8 FillStatus (1=partial, 2=final – KEIN NoOrderFills-Flag)
|
||||
# 257 char[96] InfoText (z.B. "Rithmic Direct - DTC (Filled)[final]")
|
||||
# 353 char[64] unbekannt
|
||||
# 416 char[64] UniqueExecutionID (z.B. "2537652419")
|
||||
|
||||
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]
|
||||
raw_price = struct.unpack_from("<d", data, 136)[0]
|
||||
price = raw_price / 100.0 # Rithmic liefert price × 100
|
||||
qty = struct.unpack_from("<d", data, 152)[0]
|
||||
buysell = struct.unpack_from("<I", data, 128)[0]
|
||||
total = struct.unpack_from("<i", data, 8)[0]
|
||||
msgno = struct.unpack_from("<i", data, 12)[0]
|
||||
return {
|
||||
"req_id": struct.unpack_from("<i", data, 4)[0],
|
||||
"total": total,
|
||||
"msgno": msgno,
|
||||
"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 "",
|
||||
"no_fills": total > 0 and msgno >= total,
|
||||
"unique_id": _unpack_str(data, 416, 64) if len(data) >= 480 else "",
|
||||
}
|
||||
except struct.error:
|
||||
return None
|
||||
@@ -193,7 +197,7 @@ def _parse_order_update(data):
|
||||
return None
|
||||
try:
|
||||
qty = struct.unpack_from("<d", data, 320)[0]
|
||||
price = struct.unpack_from("<d", data, 304)[0]
|
||||
price = struct.unpack_from("<d", data, 304)[0] / 100.0
|
||||
if qty <= 0 or price <= 0:
|
||||
return None # not a fill event
|
||||
uid = _unpack_str(data, 328, 64) if len(data) >= 392 else ""
|
||||
@@ -258,22 +262,14 @@ class DTCClient:
|
||||
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:
|
||||
if not f or f["req_id"] != req_id:
|
||||
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:
|
||||
if f["buysell"] in (_BUY, _SELL) and f["qty"] > 0 and f["price"] > 0:
|
||||
fills.append(f)
|
||||
if f["no_fills"]:
|
||||
break
|
||||
return fills
|
||||
|
||||
def subscribe_order_updates(self):
|
||||
@@ -400,8 +396,14 @@ def _pending_to_trade(fills, account, symbol, instruments):
|
||||
else:
|
||||
execs = f"Sell {lots} @ {ep:.4f} / {entry_str},Buy {lots} @ {xp:.4f} / {exit_str}"
|
||||
|
||||
# Trade-ID uses only actual fill datetimes — never datetime.now().
|
||||
# This makes IDs identical whether generated from the historical-fill path
|
||||
# or from the real-time ORDER_UPDATE path for the same trade.
|
||||
_id_ts = entry_ts or exit_ts
|
||||
_id_str = _id_ts.strftime(_fmt) if _id_ts else ""
|
||||
|
||||
return {
|
||||
"trade_id": f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{entry_str}",
|
||||
"trade_id": f"{account}_{symbol}_{ep:.4f}_{xp:.4f}_{_id_str}",
|
||||
"instrument": symbol,
|
||||
"direction": direction,
|
||||
"lots": lots,
|
||||
|
||||
Reference in New Issue
Block a user