Files
sentiment-engine/MALKHUT/malkhut/ipc/zinc_plane.py
Codex 6fb55dadcb malkhut(T5): risk gate + ASEx + IPC + storage + venue adapter
Risk gate (risk/gate.py): hard invariants — leverage, self-trade, post-only.
ASEx integration (execution/asex_integration.py): validate-before-mutate,
single-writer, zero-lock state serialization.
IPC (ipc/): Zinc SHM (zinc_plane.py) + control plane (control_plane.py),
UVZINC01 seqlock framing.
ClickHouse storage (storage/ch_store.py): 5 tables, HTTP API.
BingX venue adapter (venue/bingx/adapter.py): wraps DITAv2, order tracking.
2026-07-11 10:31:31 +02:00

166 lines
5.5 KiB
Python

"""
Zinc shared memory IPC layer for MALKHUT.
Uses the real Zinc shared memory adapter (POSIX SHM via /dev/shm/zinc_*),
NOT the file-based transport. This is the lock-free, zero-copy IPC path.
Regions (all use UVZINC01 seqlock framing for torn-read safety):
malkhut_book_state — canonical order book snapshot
malkhut_account_state — account/position snapshot
malkhut_fulfilment_out — planner output: action + distribution
malkhut_risk_gate — risk gate decisions
Writer: data feed / CWM
Readers: planner, risk gate, other systems
"""
from __future__ import annotations
import json
import struct
import time
from dataclasses import dataclass
from typing import Any, Mapping, Optional
# Zinc shared memory adapter (POSIX SHM, not file transport)
import sys
_ZINC_PATH = "/mnt/dolphinng5_predict/zinc/adapters/python"
if _ZINC_PATH not in sys.path:
sys.path.insert(0, _ZINC_PATH)
from zinc import SharedRegion
# UVZINC01 envelope: 8-byte magic + 8-byte seq + 8-byte json_size
_MAGIC = b"UVZINC01"
_HDR_FMT = "<8sQQ" # magic(8) + seq(u64) + json_size(u64)
_HDR_SIZE = struct.calcsize(_HDR_FMT)
DEFAULT_REGION_SIZE = 4 << 20 # 4 MiB per region
def _encode_payload(data: Mapping[str, Any], seq: int) -> bytes:
"""Encode data into UVZINC01 envelope."""
body = json.dumps(data, separators=(",", ":")).encode("utf-8")
header = struct.pack(_HDR_FMT, _MAGIC, seq, len(body))
return header + body
def _decode_payload(buf: memoryview) -> tuple[dict, int]:
"""Decode UVZINC01 envelope. Returns (data, seq). Raises on torn frame."""
if len(buf) < _HDR_SIZE:
raise ValueError("buffer too small for header")
magic, seq, json_size = struct.unpack_from(_HDR_FMT, buf)
if magic != _MAGIC:
raise ValueError(f"bad magic: {magic!r}, expected {_MAGIC!r}")
end = _HDR_SIZE + json_size
if end > len(buf):
raise ValueError(f"json_size={json_size} exceeds buffer")
body = bytes(buf[_HDR_SIZE:end])
data = json.loads(body)
return data, seq
class SharedRegionWriter:
"""Lock-free writer to a Zinc shared memory region."""
def __init__(self, region_name: str, capacity: int = DEFAULT_REGION_SIZE) -> None:
self.region_name = region_name
self.capacity = capacity
try:
self._region = SharedRegion.create(region_name, capacity)
except FileExistsError:
self._region = SharedRegion.open(region_name)
self._seq = 0
def write(self, data: Mapping[str, Any]) -> None:
self._seq += 1
payload = _encode_payload(data, self._seq)
if len(payload) > self.capacity:
raise ValueError(f"payload {len(payload)} > capacity {self.capacity}")
buf = self._region.as_buffer()
buf[:len(payload)] = payload
self._region.notify()
def close(self) -> None:
self._region.close()
class SharedRegionReader:
"""Lock-free reader from a Zinc shared memory region."""
def __init__(self, region_name: str) -> None:
self.region_name = region_name
self._region = SharedRegion.open(region_name)
def read(self, timeout_ms: int = 100) -> tuple[dict, int]:
"""Read current data. Returns (data, seq). Blocks up to timeout_ms."""
self._region.wait(timeout_ms)
buf = self._region.as_buffer()
return _decode_payload(buf)
def close(self) -> None:
self._region.close()
class MalkhutZincPlane:
"""
MALKHUT shared memory plane.
Creates dedicated Zinc regions for book state, account state,
fulfilment output, and risk gate decisions.
"""
def __init__(self, prefix: str = "malkhut", capacity: int = DEFAULT_REGION_SIZE) -> None:
self.prefix = prefix
self._writers: dict[str, SharedRegionWriter] = {}
self._readers: dict[str, SharedRegionReader] = {}
self._seq: dict[str, int] = {}
def _region_name(self, kind: str) -> str:
return f"{self.prefix}_{kind}"
def writer(self, kind: str) -> SharedRegionWriter:
if kind not in self._writers:
name = self._region_name(kind)
self._writers[kind] = SharedRegionWriter(name, DEFAULT_REGION_SIZE)
self._seq[kind] = 0
return self._writers[kind]
def reader(self, kind: str) -> SharedRegionReader:
if kind not in self._readers:
name = self._region_name(kind)
self._readers[kind] = SharedRegionReader(name)
return self._readers[kind]
def publish_book(self, data: Mapping[str, Any]) -> None:
self.writer("book_state").write(data)
def read_book(self, timeout_ms: int = 100) -> tuple[dict, int]:
return self.reader("book_state").read(timeout_ms)
def publish_account(self, data: Mapping[str, Any]) -> None:
self.writer("account_state").write(data)
def read_account(self, timeout_ms: int = 100) -> tuple[dict, int]:
return self.reader("account_state").read(timeout_ms)
def publish_fulfilment(self, data: Mapping[str, Any]) -> None:
self.writer("fulfilment_out").write(data)
def read_fulfilment(self, timeout_ms: int = 100) -> tuple[dict, int]:
return self.reader("fulfilment_out").read(timeout_ms)
def publish_risk(self, data: Mapping[str, Any]) -> None:
self.writer("risk_gate").write(data)
def read_risk(self, timeout_ms: int = 100) -> tuple[dict, int]:
return self.reader("risk_gate").read(timeout_ms)
def close_all(self) -> None:
for w in self._writers.values():
w.close()
for r in self._readers.values():
r.close()