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.
This commit is contained in:
2
MALKHUT/malkhut/ipc/__init__.py
Normal file
2
MALKHUT/malkhut/ipc/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from malkhut.ipc.zinc_plane import MalkhutZincPlane
|
||||
from malkhut.ipc.control_plane import MalkhutControlPlane
|
||||
139
MALKHUT/malkhut/ipc/control_plane.py
Normal file
139
MALKHUT/malkhut/ipc/control_plane.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
CONTROL_PLANE — shared memory region for commands, target symbols,
|
||||
venue lifecycle management, and system management.
|
||||
|
||||
This is a SEPARATE region from the data plane (book/account/fulfilment).
|
||||
External systems communicate with MALKHUT via this region.
|
||||
|
||||
All writes are atomic (single UVZINC01 frame). All reads are lock-free.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Mapping, Optional, Sequence
|
||||
|
||||
from malkhut.ipc.zinc_plane import (
|
||||
SharedRegionReader,
|
||||
SharedRegionWriter,
|
||||
_decode_payload,
|
||||
_encode_payload,
|
||||
DEFAULT_REGION_SIZE,
|
||||
_HDR_SIZE,
|
||||
)
|
||||
|
||||
|
||||
class ControlCommand(str, Enum):
|
||||
START = "START"
|
||||
STOP = "STOP"
|
||||
PAUSE = "PAUSE"
|
||||
RESUME = "RESUME"
|
||||
SET_SYMBOLS = "SET_SYMBOLS"
|
||||
CONNECT_VENUE = "CONNECT_VENUE"
|
||||
DISCONNECT_VENUE = "DISCONNECT_VENUE"
|
||||
SET_MODE = "SET_MODE"
|
||||
EMERGENCY_STOP = "EMERGENCY_STOP"
|
||||
HOT_RELOAD_POLICY = "HOT_RELOAD_POLICY"
|
||||
STATUS_REQUEST = "STATUS_REQUEST"
|
||||
|
||||
|
||||
class VenueLifecycle(str, Enum):
|
||||
DISCONNECTED = "DISCONNECTED"
|
||||
CONNECTING = "CONNECTING"
|
||||
CONNECTED = "CONNECTED"
|
||||
RECONNECTING = "RECONNECTING"
|
||||
ERROR = "ERROR"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ControlPlaneFrame:
|
||||
"""Single atomic control plane frame."""
|
||||
command: str
|
||||
ts_ns: int
|
||||
target_symbols: tuple[str, ...] = ()
|
||||
venue: str = ""
|
||||
venue_lifecycle: str = ""
|
||||
mode: str = ""
|
||||
policy_version: str = ""
|
||||
params: Mapping[str, Any] = field(default_factory=dict)
|
||||
source: str = ""
|
||||
ack_required: bool = False
|
||||
|
||||
|
||||
class MalkhutControlPlane:
|
||||
"""
|
||||
CONTROL_PLANE shared memory region.
|
||||
|
||||
External systems (other agents, TUI, supervisor, venue adapters)
|
||||
write commands here. MALKHUT reads and processes them.
|
||||
|
||||
Region name: malkhut_control (Zinc object: /dev/shm/zinc_malkhut_control)
|
||||
|
||||
Protocol:
|
||||
1. External system writes a ControlPlaneFrame to the region.
|
||||
2. MALKHUT reads the frame, processes the command.
|
||||
3. If ack_required, MALKHUT writes an ACK frame back.
|
||||
"""
|
||||
|
||||
REGION_NAME = "malkhut_control"
|
||||
|
||||
def __init__(self, capacity: int = DEFAULT_REGION_SIZE) -> None:
|
||||
self._writer = SharedRegionWriter(self.REGION_NAME, capacity)
|
||||
self._reader = SharedRegionReader(self.REGION_NAME)
|
||||
self._seq = 0
|
||||
|
||||
def publish_command(self, frame: ControlPlaneFrame) -> None:
|
||||
"""Write a control command to the region."""
|
||||
self._seq += 1
|
||||
data = {
|
||||
"command": frame.command,
|
||||
"ts_ns": frame.ts_ns,
|
||||
"target_symbols": list(frame.target_symbols),
|
||||
"venue": frame.venue,
|
||||
"venue_lifecycle": frame.venue_lifecycle,
|
||||
"mode": frame.mode,
|
||||
"policy_version": frame.policy_version,
|
||||
"params": dict(frame.params),
|
||||
"source": frame.source,
|
||||
"ack_required": frame.ack_required,
|
||||
"seq": self._seq,
|
||||
}
|
||||
self._writer.write(data)
|
||||
|
||||
def read_command(self, timeout_ms: int = 100) -> Optional[ControlPlaneFrame]:
|
||||
"""Read latest control command. Returns None on timeout/error."""
|
||||
try:
|
||||
data, seq = self._reader.read(timeout_ms)
|
||||
return ControlPlaneFrame(
|
||||
command=data.get("command", ""),
|
||||
ts_ns=data.get("ts_ns", 0),
|
||||
target_symbols=tuple(data.get("target_symbols", [])),
|
||||
venue=data.get("venue", ""),
|
||||
venue_lifecycle=data.get("venue_lifecycle", ""),
|
||||
mode=data.get("mode", ""),
|
||||
policy_version=data.get("policy_version", ""),
|
||||
params=data.get("params", {}),
|
||||
source=data.get("source", ""),
|
||||
ack_required=data.get("ack_required", False),
|
||||
)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return None
|
||||
|
||||
def publish_ack(self, command: str, ts_ns: int, status: str, details: str = "") -> None:
|
||||
"""Write an ACK frame back to the control plane."""
|
||||
self._seq += 1
|
||||
data = {
|
||||
"command": f"ACK_{command}",
|
||||
"ts_ns": ts_ns,
|
||||
"status": status,
|
||||
"details": details,
|
||||
"seq": self._seq,
|
||||
}
|
||||
self._writer.write(data)
|
||||
|
||||
def close(self) -> None:
|
||||
self._writer.close()
|
||||
self._reader.close()
|
||||
165
MALKHUT/malkhut/ipc/zinc_plane.py
Normal file
165
MALKHUT/malkhut/ipc/zinc_plane.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user