repo hygiene: track the PINK launcher import closure

67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-12 15:09:32 +02:00
parent c3a18f693a
commit 84e4a50e3f
67 changed files with 15090 additions and 0 deletions

106
prod/bingx/rate_limits.py Normal file
View File

@@ -0,0 +1,106 @@
from __future__ import annotations
from dataclasses import dataclass
from time import monotonic_ns
@dataclass(frozen=True)
class BingxRateLimitSnapshot:
rest_remaining: int | None = None
rest_reset_ms: int | None = None
ws_listenkey_ops: int = 0
ws_listenkey_window_ns: int = 0
@dataclass(frozen=True)
class BingxCircuitBreakerSnapshot:
failure_count: int = 0
open_until_ns: int = 0
last_delay_ms: int = 0
@property
def is_open(self) -> bool:
return monotonic_ns() < self.open_until_ns
class BingxRateLimitTracker:
def __init__(self) -> None:
self._rest_remaining: int | None = None
self._rest_reset_ms: int | None = None
self._ws_listenkey_ops = 0
self._ws_window_start_ns = monotonic_ns()
def update_rest_headers(self, headers: dict[str, str]) -> None:
remain = headers.get("x-ratelimit-requests-remain")
reset = headers.get("x-ratelimit-requests-expire")
self._rest_remaining = int(remain) if remain is not None and str(remain).isdigit() else self._rest_remaining
self._rest_reset_ms = int(reset) if reset is not None and str(reset).isdigit() else self._rest_reset_ms
def count_ws_listenkey_op(self) -> None:
now = monotonic_ns()
if now - self._ws_window_start_ns > 1_000_000_000:
self._ws_window_start_ns = now
self._ws_listenkey_ops = 0
self._ws_listenkey_ops += 1
def snapshot(self) -> BingxRateLimitSnapshot:
return BingxRateLimitSnapshot(
rest_remaining=self._rest_remaining,
rest_reset_ms=self._rest_reset_ms,
ws_listenkey_ops=self._ws_listenkey_ops,
ws_listenkey_window_ns=monotonic_ns() - self._ws_window_start_ns,
)
class BingxCircuitBreaker:
def __init__(
self,
*,
failure_threshold: int = 3,
base_backoff_ms: int = 250,
max_backoff_ms: int = 2_000,
) -> None:
self._failure_threshold = max(1, int(failure_threshold))
self._base_backoff_ms = max(1, int(base_backoff_ms))
self._max_backoff_ms = max(self._base_backoff_ms, int(max_backoff_ms))
self._failure_count = 0
self._open_until_ns = 0
self._last_delay_ms = 0
async def wait_if_open(self) -> None:
remaining = self.open_remaining_secs()
if remaining > 0:
from asyncio import sleep
await sleep(remaining)
def open_remaining_secs(self) -> float:
remaining_ns = self._open_until_ns - monotonic_ns()
return max(0.0, remaining_ns / 1_000_000_000)
def snapshot(self) -> BingxCircuitBreakerSnapshot:
return BingxCircuitBreakerSnapshot(
failure_count=self._failure_count,
open_until_ns=self._open_until_ns,
last_delay_ms=self._last_delay_ms,
)
def record_success(self) -> None:
self._failure_count = 0
self._open_until_ns = 0
self._last_delay_ms = 0
def record_failure(self, *, rate_limited: bool = False, retry_after_ms: int | None = None) -> float:
now_ns = monotonic_ns()
if rate_limited and retry_after_ms is not None and retry_after_ms > 0:
self._failure_count = self._failure_threshold
self._last_delay_ms = retry_after_ms
self._open_until_ns = max(self._open_until_ns, now_ns + retry_after_ms * 1_000_000)
return retry_after_ms / 1000.0
self._failure_count += 1
delay_ms = min(self._base_backoff_ms * (2 ** (self._failure_count - 1)), self._max_backoff_ms)
self._last_delay_ms = delay_ms
if self._failure_count >= self._failure_threshold:
self._open_until_ns = max(self._open_until_ns, now_ns + delay_ms * 1_000_000)
return delay_ms / 1000.0