dita_v2: reconcile SOA — vendored thread-safety hardening ∪ venue plane
3-way merge (base=pre-venue-plane upstream, ours=violet-main vendored hardening, theirs=upstream venue plane): RLock atomic-snapshot wrapping (account.py, real_control_plane, real_zinc_plane), lazy __getattr__ imports (__init__), account-core test coverage — merged with venue_region telemetry. Zero conflicts; all files AST-verified. Ends the two-way vendor drift found in DITAV2_SOA_SURVEY_20260702 §5 / UV_DITAV2_SOA_VERDICT_20260703. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,8 +36,6 @@ from .contracts import (
|
|||||||
)
|
)
|
||||||
from .journal import ClickHouseKernelJournal, KernelJournal, MemoryKernelJournal
|
from .journal import ClickHouseKernelJournal, KernelJournal, MemoryKernelJournal
|
||||||
from .rust_backend import ExecutionKernel
|
from .rust_backend import ExecutionKernel
|
||||||
from .bingx_venue import BingxVenueAdapter
|
|
||||||
from .launcher import DITAv2LauncherBundle, LauncherVenueMode, LauncherZincMode, build_launcher_bundle
|
|
||||||
from .projection import HazelcastProjection, build_position_state_row, build_projection
|
from .projection import HazelcastProjection, build_position_state_row, build_projection
|
||||||
from .venue import VenueAdapter
|
from .venue import VenueAdapter
|
||||||
from .mock_venue import MockVenueAdapter, MockVenueScenario
|
from .mock_venue import MockVenueAdapter, MockVenueScenario
|
||||||
@@ -45,6 +43,28 @@ from .zinc_plane import InMemoryZincPlane, ZincPlane
|
|||||||
from .real_zinc_plane import RealZincPlane, RealZincUnavailable
|
from .real_zinc_plane import RealZincPlane, RealZincUnavailable
|
||||||
from .real_control_plane import RealZincControlPlane, RealZincUnavailable as RealZincControlUnavailable
|
from .real_control_plane import RealZincControlPlane, RealZincUnavailable as RealZincControlUnavailable
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if name == "BingxVenueAdapter":
|
||||||
|
from .bingx_venue import BingxVenueAdapter
|
||||||
|
|
||||||
|
return BingxVenueAdapter
|
||||||
|
if name in {"DITAv2LauncherBundle", "LauncherVenueMode", "LauncherZincMode", "build_launcher_bundle"}:
|
||||||
|
from .launcher import (
|
||||||
|
DITAv2LauncherBundle,
|
||||||
|
LauncherVenueMode,
|
||||||
|
LauncherZincMode,
|
||||||
|
build_launcher_bundle,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"DITAv2LauncherBundle": DITAv2LauncherBundle,
|
||||||
|
"LauncherVenueMode": LauncherVenueMode,
|
||||||
|
"LauncherZincMode": LauncherZincMode,
|
||||||
|
"build_launcher_bundle": build_launcher_bundle,
|
||||||
|
}[name]
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AccountProjection",
|
"AccountProjection",
|
||||||
"AccountSnapshot",
|
"AccountSnapshot",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from enum import Enum
|
|||||||
from typing import Any, Dict, Iterable, List, Optional
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
from .contracts import TradeSide, TradeSlot, TradeStage
|
from .contracts import TradeSide, TradeSlot, TradeStage
|
||||||
from .utils import safe_float
|
from .utils import safe_float
|
||||||
@@ -59,42 +60,49 @@ class AccountProjection:
|
|||||||
GIL guarantees single-field reference assignment is atomic, so readers
|
GIL guarantees single-field reference assignment is atomic, so readers
|
||||||
that hold snap = kernel.account.snapshot before use see a consistent view.
|
that hold snap = kernel.account.snapshot before use see a consistent view.
|
||||||
"""
|
"""
|
||||||
cur = self.snapshot
|
with self._lock:
|
||||||
self.snapshot = AccountSnapshot(
|
cur = self.snapshot
|
||||||
capital=kw.get("capital", cur.capital),
|
self.snapshot = AccountSnapshot(
|
||||||
equity=kw.get("equity", cur.equity),
|
capital=kw.get("capital", cur.capital),
|
||||||
realized_pnl=kw.get("realized_pnl", cur.realized_pnl),
|
equity=kw.get("equity", cur.equity),
|
||||||
unrealized_pnl=kw.get("unrealized_pnl", cur.unrealized_pnl),
|
realized_pnl=kw.get("realized_pnl", cur.realized_pnl),
|
||||||
open_positions=kw.get("open_positions", cur.open_positions),
|
unrealized_pnl=kw.get("unrealized_pnl", cur.unrealized_pnl),
|
||||||
open_notional=kw.get("open_notional", cur.open_notional),
|
open_positions=kw.get("open_positions", cur.open_positions),
|
||||||
fees_paid=kw.get("fees_paid", cur.fees_paid),
|
open_notional=kw.get("open_notional", cur.open_notional),
|
||||||
trade_seq=kw.get("trade_seq", cur.trade_seq),
|
fees_paid=kw.get("fees_paid", cur.fees_paid),
|
||||||
peak_capital=kw.get("peak_capital", cur.peak_capital),
|
trade_seq=kw.get("trade_seq", cur.trade_seq),
|
||||||
capital_source=kw.get("capital_source", cur.capital_source),
|
peak_capital=kw.get("peak_capital", cur.peak_capital),
|
||||||
e_wallet_balance=kw.get("e_wallet_balance", cur.e_wallet_balance),
|
capital_source=kw.get("capital_source", cur.capital_source),
|
||||||
event_seq=kw.get("event_seq", cur.event_seq),
|
e_wallet_balance=kw.get("e_wallet_balance", cur.e_wallet_balance),
|
||||||
)
|
event_seq=kw.get("event_seq", cur.event_seq),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
def observe_slots(self, slots: Iterable[TradeSlot]) -> None:
|
def observe_slots(self, slots: Iterable[TradeSlot]) -> None:
|
||||||
open_positions = 0
|
with self._lock:
|
||||||
open_notional = 0.0
|
open_positions = 0
|
||||||
unrealized_pnl = 0.0
|
open_notional = 0.0
|
||||||
for slot in slots:
|
unrealized_pnl = 0.0
|
||||||
if slot.closed or slot.size <= 0:
|
for slot in slots:
|
||||||
continue
|
if slot.closed or slot.size <= 0:
|
||||||
if slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED, TradeStage.ENTRY_WORKING, TradeStage.EXIT_WORKING}:
|
continue
|
||||||
open_positions += 1
|
if slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED, TradeStage.ENTRY_WORKING, TradeStage.EXIT_WORKING}:
|
||||||
mark = safe_float(slot.entry_price, 0.0)
|
open_positions += 1
|
||||||
mark = safe_float(slot.metadata.get("mark_price"), mark)
|
mark = safe_float(slot.entry_price, 0.0)
|
||||||
open_notional += abs(slot.size) * abs(mark)
|
mark = safe_float(slot.metadata.get("mark_price"), mark)
|
||||||
unrealized_pnl += float(slot.unrealized_pnl or 0.0)
|
open_notional += abs(slot.size) * abs(mark)
|
||||||
self._replace_snapshot(
|
unrealized_pnl += float(slot.unrealized_pnl or 0.0)
|
||||||
open_positions=open_positions,
|
capital = self.snapshot.capital
|
||||||
open_notional=open_notional,
|
peak_capital = self.snapshot.peak_capital
|
||||||
unrealized_pnl=unrealized_pnl,
|
self._replace_snapshot(
|
||||||
equity=self.snapshot.capital + unrealized_pnl if math.isfinite(self.snapshot.capital + unrealized_pnl) else self.snapshot.capital,
|
open_positions=open_positions,
|
||||||
peak_capital=max(self.snapshot.peak_capital, self.snapshot.capital) if open_notional > 0 and self.snapshot.capital > 0 else self.snapshot.peak_capital,
|
open_notional=open_notional,
|
||||||
)
|
unrealized_pnl=unrealized_pnl,
|
||||||
|
equity=capital + unrealized_pnl if math.isfinite(capital + unrealized_pnl) else capital,
|
||||||
|
peak_capital=max(peak_capital, capital) if open_notional > 0 and capital > 0 else peak_capital,
|
||||||
|
)
|
||||||
|
|
||||||
def anchor_to_exchange(self, wallet_balance: float, available_margin: float, event_seq: int) -> None:
|
def anchor_to_exchange(self, wallet_balance: float, available_margin: float, event_seq: int) -> None:
|
||||||
"""Snap published capital to exchange wallet balance.
|
"""Snap published capital to exchange wallet balance.
|
||||||
@@ -106,39 +114,42 @@ class AccountProjection:
|
|||||||
Guards: wallet_balance must be > 0 and finite (the zero-wb frame lesson
|
Guards: wallet_balance must be > 0 and finite (the zero-wb frame lesson
|
||||||
from ACCOUNT_UPDATE frames with no USDT balance entry).
|
from ACCOUNT_UPDATE frames with no USDT balance entry).
|
||||||
"""
|
"""
|
||||||
wb = safe_float(wallet_balance, 0.0)
|
with self._lock:
|
||||||
if wb <= 0.0 or not math.isfinite(wb):
|
wb = safe_float(wallet_balance, 0.0)
|
||||||
return
|
if wb <= 0.0 or not math.isfinite(wb):
|
||||||
self.snapshot.capital = wb
|
return
|
||||||
self.snapshot.e_wallet_balance = wb
|
self.snapshot.capital = wb
|
||||||
self.snapshot.capital_source = "e_anchored"
|
self.snapshot.e_wallet_balance = wb
|
||||||
self.snapshot.event_seq = int(event_seq)
|
self.snapshot.capital_source = "e_anchored"
|
||||||
self.snapshot.equity = wb + self.snapshot.unrealized_pnl
|
self.snapshot.event_seq = int(event_seq)
|
||||||
if not math.isfinite(self.snapshot.equity):
|
self.snapshot.equity = wb + self.snapshot.unrealized_pnl
|
||||||
self.snapshot.equity = wb
|
if not math.isfinite(self.snapshot.equity):
|
||||||
self.snapshot.peak_capital = max(self.snapshot.peak_capital, wb)
|
self.snapshot.equity = wb
|
||||||
|
self.snapshot.peak_capital = max(self.snapshot.peak_capital, wb)
|
||||||
|
|
||||||
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
|
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
|
||||||
rp = safe_float(realized_pnl, 0.0)
|
with self._lock:
|
||||||
# Include fees in capital delta (today fees only accumulate in
|
cur = self.snapshot
|
||||||
# fees_paid while published capital ignores them between reseeds).
|
rp = safe_float(realized_pnl, 0.0)
|
||||||
net = rp - safe_float(fees, 0.0)
|
# Include fees in capital delta (today fees only accumulate in
|
||||||
new_capital = safe_float(self.snapshot.capital + net, self.snapshot.capital)
|
# fees_paid while published capital ignores them between reseeds).
|
||||||
if self.max_capital is not None:
|
net = rp - safe_float(fees, 0.0)
|
||||||
new_capital = min(new_capital, self.max_capital)
|
new_capital = safe_float(cur.capital + net, cur.capital)
|
||||||
new_capital = max(self.min_capital, new_capital)
|
if self.max_capital is not None:
|
||||||
new_source = self.snapshot.capital_source
|
new_capital = min(new_capital, self.max_capital)
|
||||||
if new_source == "e_anchored" and abs(net) > 1e-12:
|
new_capital = max(self.min_capital, new_capital)
|
||||||
new_source = "k_bridged"
|
new_source = cur.capital_source
|
||||||
new_fees = self.snapshot.fees_paid + safe_float(fees, 0.0)
|
if new_source == "e_anchored" and abs(net) > 1e-12:
|
||||||
new_equity = new_capital + self.snapshot.unrealized_pnl
|
new_source = "k_bridged"
|
||||||
if not math.isfinite(new_equity):
|
new_fees = cur.fees_paid + safe_float(fees, 0.0)
|
||||||
new_equity = new_capital
|
new_equity = new_capital + cur.unrealized_pnl
|
||||||
self._replace_snapshot(
|
if not math.isfinite(new_equity):
|
||||||
capital=new_capital, capital_source=new_source,
|
new_equity = new_capital
|
||||||
realized_pnl=self.snapshot.realized_pnl + rp,
|
self._replace_snapshot(
|
||||||
fees_paid=new_fees, equity=new_equity,
|
capital=new_capital, capital_source=new_source,
|
||||||
)
|
realized_pnl=cur.realized_pnl + rp,
|
||||||
|
fees_paid=new_fees, equity=new_equity,
|
||||||
|
)
|
||||||
|
|
||||||
def to_account_event(
|
def to_account_event(
|
||||||
self,
|
self,
|
||||||
@@ -154,31 +165,32 @@ class AccountProjection:
|
|||||||
bars_held: int = 0,
|
bars_held: int = 0,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
|
with self._lock:
|
||||||
return {
|
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
|
||||||
"timestamp": timestamp.isoformat() if hasattr(timestamp, "isoformat") else str(timestamp),
|
return {
|
||||||
"runtime_namespace": self.runtime_namespace,
|
"timestamp": timestamp.isoformat() if hasattr(timestamp, "isoformat") else str(timestamp),
|
||||||
"strategy_namespace": self.strategy_namespace,
|
"runtime_namespace": self.runtime_namespace,
|
||||||
"event_namespace": self.event_namespace,
|
"strategy_namespace": self.strategy_namespace,
|
||||||
"actor_name": self.actor_name,
|
"event_namespace": self.event_namespace,
|
||||||
"exec_venue": self.exec_venue,
|
"actor_name": self.actor_name,
|
||||||
"data_venue": self.data_venue,
|
"exec_venue": self.exec_venue,
|
||||||
"ledger_authority": self.ledger_authority,
|
"data_venue": self.data_venue,
|
||||||
"capital": float(self.snapshot.capital),
|
"ledger_authority": self.ledger_authority,
|
||||||
"equity": float(self.snapshot.equity),
|
"capital": float(self.snapshot.capital),
|
||||||
"open_positions": int(self.snapshot.open_positions),
|
"equity": float(self.snapshot.equity),
|
||||||
"current_open_notional": float(self.snapshot.open_notional),
|
"open_positions": int(self.snapshot.open_positions),
|
||||||
"current_account_leverage": float(self.snapshot.leverage),
|
"current_open_notional": float(self.snapshot.open_notional),
|
||||||
"trade_id": trade_id,
|
"current_account_leverage": float(self.snapshot.leverage),
|
||||||
"asset": asset,
|
"trade_id": trade_id,
|
||||||
"side": side.value,
|
"asset": asset,
|
||||||
"reason": reason,
|
"side": side.value,
|
||||||
"stage": stage.value,
|
"reason": reason,
|
||||||
"pnl": float(pnl),
|
"stage": stage.value,
|
||||||
"pnl_pct": float(pnl_pct),
|
"pnl": float(pnl),
|
||||||
"bars_held": int(bars_held),
|
"pnl_pct": float(pnl_pct),
|
||||||
"metadata": dict(metadata or {}),
|
"bars_held": int(bars_held),
|
||||||
}
|
"metadata": dict(metadata or {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -311,6 +323,7 @@ class AccountProjectionV2:
|
|||||||
self._min_capital = min_capital
|
self._min_capital = min_capital
|
||||||
self._max_capital = max_capital
|
self._max_capital = max_capital
|
||||||
self._cfg = reconcile_config or ReconcileConfig()
|
self._cfg = reconcile_config or ReconcileConfig()
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
# Running K-value accumulators
|
# Running K-value accumulators
|
||||||
self._k_realized: float = 0.0
|
self._k_realized: float = 0.0
|
||||||
@@ -345,16 +358,18 @@ class AccountProjectionV2:
|
|||||||
fee: float,
|
fee: float,
|
||||||
realized_pnl: float,
|
realized_pnl: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._k_realized += _safe(realized_pnl)
|
with self._lock:
|
||||||
self._k_fees += _safe(fee)
|
self._k_realized += _safe(realized_pnl)
|
||||||
self._e_last_fill_price = _safe(fill_price)
|
self._k_fees += _safe(fee)
|
||||||
self._e_last_fill_qty = _safe(fill_qty)
|
self._e_last_fill_price = _safe(fill_price)
|
||||||
self._e_last_fill_fee = _safe(fee)
|
self._e_last_fill_qty = _safe(fill_qty)
|
||||||
self._e_last_fill_realized = _safe(realized_pnl)
|
self._e_last_fill_fee = _safe(fee)
|
||||||
|
self._e_last_fill_realized = _safe(realized_pnl)
|
||||||
|
|
||||||
def apply_funding(self, amount: float) -> None:
|
def apply_funding(self, amount: float) -> None:
|
||||||
self._k_funding += _safe(amount)
|
with self._lock:
|
||||||
self._e_last_funding = _safe(amount)
|
self._k_funding += _safe(amount)
|
||||||
|
self._e_last_funding = _safe(amount)
|
||||||
|
|
||||||
def apply_balance_update(
|
def apply_balance_update(
|
||||||
self,
|
self,
|
||||||
@@ -364,13 +379,15 @@ class AccountProjectionV2:
|
|||||||
used_margin: float,
|
used_margin: float,
|
||||||
maint_margin: float,
|
maint_margin: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._e_wallet_balance = _safe(wallet_balance)
|
with self._lock:
|
||||||
self._e_avail_margin = _safe(available_margin)
|
self._e_wallet_balance = _safe(wallet_balance)
|
||||||
self._e_used_margin = _safe(used_margin)
|
self._e_avail_margin = _safe(available_margin)
|
||||||
self._e_maint_margin = _safe(maint_margin)
|
self._e_used_margin = _safe(used_margin)
|
||||||
|
self._e_maint_margin = _safe(maint_margin)
|
||||||
|
|
||||||
def apply_position_update(self, positions: List[EPosition]) -> None:
|
def apply_position_update(self, positions: List[EPosition]) -> None:
|
||||||
self._e_positions = list(positions)
|
with self._lock:
|
||||||
|
self._e_positions = list(positions)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Snapshot construction (called after each ingestion step)
|
# Snapshot construction (called after each ingestion step)
|
||||||
@@ -382,21 +399,24 @@ class AccountProjectionV2:
|
|||||||
slots: Iterable[TradeSlot],
|
slots: Iterable[TradeSlot],
|
||||||
ts: Optional[float] = None,
|
ts: Optional[float] = None,
|
||||||
) -> AccountSnapshotV2:
|
) -> AccountSnapshotV2:
|
||||||
self._event_seq += 1
|
with self._lock:
|
||||||
snap = self._build(self._event_seq, source_event_id, list(slots), ts or time.time())
|
self._event_seq += 1
|
||||||
self._snapshot = snap
|
snap = self._build(self._event_seq, source_event_id, list(slots), ts or time.time())
|
||||||
return snap
|
self._snapshot = snap
|
||||||
|
return snap
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def snapshot(self) -> AccountSnapshotV2:
|
def snapshot(self) -> AccountSnapshotV2:
|
||||||
return self._snapshot
|
with self._lock:
|
||||||
|
return self._snapshot
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def k_capital(self) -> float:
|
def k_capital(self) -> float:
|
||||||
raw = self._seed + self._k_realized - self._k_fees - self._k_funding
|
with self._lock:
|
||||||
if self._max_capital is not None:
|
raw = self._seed + self._k_realized - self._k_fees - self._k_funding
|
||||||
raw = min(raw, self._max_capital)
|
if self._max_capital is not None:
|
||||||
return max(self._min_capital, raw)
|
raw = min(raw, self._max_capital)
|
||||||
|
return max(self._min_capital, raw)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Internal helpers
|
# Internal helpers
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
from .control import BackendMode, ControlPlane, ControlUpdate, KernelControlSnapshot, KernelMode, KernelVerbosity
|
from .control import BackendMode, ControlPlane, ControlUpdate, KernelControlSnapshot, KernelMode, KernelVerbosity
|
||||||
|
|
||||||
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
|
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
|
||||||
@@ -70,6 +72,7 @@ class RealZincControlPlane(ControlPlane):
|
|||||||
require_real_zinc()
|
require_real_zinc()
|
||||||
base = prefix.strip("/").replace("/", "_")
|
base = prefix.strip("/").replace("/", "_")
|
||||||
self.region_name = f"{base}_control"
|
self.region_name = f"{base}_control"
|
||||||
|
self._lock = threading.RLock()
|
||||||
self._seq = 0
|
self._seq = 0
|
||||||
self._snapshot = KernelControlSnapshot()
|
self._snapshot = KernelControlSnapshot()
|
||||||
if create:
|
if create:
|
||||||
@@ -86,21 +89,24 @@ class RealZincControlPlane(ControlPlane):
|
|||||||
self.region.close()
|
self.region.close()
|
||||||
|
|
||||||
def read(self) -> KernelControlSnapshot:
|
def read(self) -> KernelControlSnapshot:
|
||||||
payload = _decode_packet(self.region.as_buffer())
|
with self._lock:
|
||||||
control = payload.get("control") if isinstance(payload, dict) else None
|
payload = _decode_packet(self.region.as_buffer())
|
||||||
if not isinstance(control, dict):
|
control = payload.get("control") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(control, dict):
|
||||||
|
return self._snapshot
|
||||||
|
self._snapshot = KernelControlSnapshot(**control)
|
||||||
return self._snapshot
|
return self._snapshot
|
||||||
self._snapshot = KernelControlSnapshot(**control)
|
|
||||||
return self._snapshot
|
|
||||||
|
|
||||||
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
|
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
|
||||||
self._snapshot = update.apply(self.read())
|
with self._lock:
|
||||||
self._seq += 1
|
self._snapshot = update.apply(self.read())
|
||||||
self._write_region(self._seq, self._snapshot.as_dict())
|
self._seq += 1
|
||||||
return self._snapshot
|
self._write_region(self._seq, self._snapshot.as_dict())
|
||||||
|
return self._snapshot
|
||||||
|
|
||||||
def mirror(self) -> Dict[str, Any]:
|
def mirror(self) -> Dict[str, Any]:
|
||||||
return self._snapshot.as_dict()
|
with self._lock:
|
||||||
|
return self._snapshot.as_dict()
|
||||||
|
|
||||||
def wait(self, timeout_ms: int = 1000) -> bool:
|
def wait(self, timeout_ms: int = 1000) -> bool:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -258,14 +258,16 @@ class RealZincPlane:
|
|||||||
self._write_region(self.state_region, self._state_seq, payload)
|
self._write_region(self.state_region, self._state_seq, payload)
|
||||||
|
|
||||||
def read_slots(self) -> List[TradeSlot]:
|
def read_slots(self) -> List[TradeSlot]:
|
||||||
payload = _decode_packet(self.state_region.as_buffer())
|
with self._lock:
|
||||||
slots = payload.get("slots", []) if isinstance(payload, dict) else []
|
payload = _decode_packet(self.state_region.as_buffer())
|
||||||
return [_slot_from_payload(slot) for slot in sorted(slots, key=lambda row: int(row.get("slot_id", 0)))]
|
slots = payload.get("slots", []) if isinstance(payload, dict) else []
|
||||||
|
return [_slot_from_payload(slot) for slot in sorted(slots, key=lambda row: int(row.get("slot_id", 0)))]
|
||||||
|
|
||||||
def read_intents(self) -> List[Dict[str, Any]]:
|
def read_intents(self) -> List[Dict[str, Any]]:
|
||||||
payload = _decode_packet(self.intent_region.as_buffer())
|
with self._lock:
|
||||||
items = payload.get("items", []) if isinstance(payload, dict) else []
|
payload = _decode_packet(self.intent_region.as_buffer())
|
||||||
return list(items)
|
items = payload.get("items", []) if isinstance(payload, dict) else []
|
||||||
|
return list(items)
|
||||||
|
|
||||||
def update_control(self, control: KernelControlSnapshot) -> None:
|
def update_control(self, control: KernelControlSnapshot) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -274,11 +276,12 @@ class RealZincPlane:
|
|||||||
self._write_region(self.control_region, self._control_seq, {"control": control.as_dict()})
|
self._write_region(self.control_region, self._control_seq, {"control": control.as_dict()})
|
||||||
|
|
||||||
def read_control(self) -> KernelControlSnapshot:
|
def read_control(self) -> KernelControlSnapshot:
|
||||||
payload = _decode_packet(self.control_region.as_buffer())
|
with self._lock:
|
||||||
control = payload.get("control") if isinstance(payload, dict) else None
|
payload = _decode_packet(self.control_region.as_buffer())
|
||||||
if not isinstance(control, dict):
|
control = payload.get("control") if isinstance(payload, dict) else None
|
||||||
return self._control_cache
|
if not isinstance(control, dict):
|
||||||
return KernelControlSnapshot(**control)
|
return self._control_cache
|
||||||
|
return KernelControlSnapshot(**control)
|
||||||
|
|
||||||
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
|
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
|
||||||
return bool(self.state_region.wait(timeout_ms))
|
return bool(self.state_region.wait(timeout_ms))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -324,7 +325,46 @@ class TestReplayDeterminism:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 7. V1 backward compatibility (AccountProjection must be untouched)
|
# 7. Concurrency guard
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestConcurrencyGuard:
|
||||||
|
def test_apply_fill_is_serialized(self):
|
||||||
|
proj = _proj(10_000.0)
|
||||||
|
n_threads = 16
|
||||||
|
per_thread = 250
|
||||||
|
total_fee = 0.0
|
||||||
|
total_realized = 0.0
|
||||||
|
|
||||||
|
def _worker(tid: int) -> tuple[float, float]:
|
||||||
|
local_fee = 0.0
|
||||||
|
local_realized = 0.0
|
||||||
|
for i in range(per_thread):
|
||||||
|
realized = float(tid * per_thread + i)
|
||||||
|
fee = float((i % 5) * 0.1)
|
||||||
|
proj.apply_fill(
|
||||||
|
fill_price=100.0,
|
||||||
|
fill_qty=1.0,
|
||||||
|
fee=fee,
|
||||||
|
realized_pnl=realized,
|
||||||
|
)
|
||||||
|
local_fee += fee
|
||||||
|
local_realized += realized
|
||||||
|
return local_realized, local_fee
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=n_threads) as ex:
|
||||||
|
for realized, fee in ex.map(_worker, range(n_threads)):
|
||||||
|
total_realized += realized
|
||||||
|
total_fee += fee
|
||||||
|
|
||||||
|
snap = _snap(proj)
|
||||||
|
assert snap.k.realized_pnl == pytest.approx(total_realized)
|
||||||
|
assert snap.k.fees_paid == pytest.approx(total_fee)
|
||||||
|
assert snap.k.capital == pytest.approx(10_000.0 + total_realized - total_fee)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 8. V1 backward compatibility (AccountProjection must be untouched)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestV1Compat:
|
class TestV1Compat:
|
||||||
|
|||||||
Reference in New Issue
Block a user