diff --git a/src/fxhnt/application/bybit_exec_record.py b/src/fxhnt/application/bybit_exec_record.py index c4903c8..d0bb501 100644 --- a/src/fxhnt/application/bybit_exec_record.py +++ b/src/fxhnt/application/bybit_exec_record.py @@ -15,11 +15,22 @@ def crypto_envelope(repo: Any, equity: float) -> float: return min(float(repo.current_allocation().get("bybit_4edge", 0.0)), float(equity)) +def paper_or_b2a_crypto_envelope(repo: Any, equity: float, paper_envelope: float, is_paper: bool) -> float: + """PAPER-validation envelope (min(paper, equity)) when `paper_envelope > 0`, bypassing B2a — refused on a + non-testnet account (never widen real-capital exposure). Otherwise the B2a/B3-gated `crypto_envelope`.""" + if paper_envelope > 0.0: + if not is_paper: + raise ValueError("paper-envelope refused: bybit account is not testnet/paper") + return min(float(paper_envelope), float(equity)) + return crypto_envelope(repo, equity) + + def build_crypto_pos_state(positions: dict[str, float], marks: dict[str, float], envelope: float, - funding_cursor_ts: int) -> dict[str, Any]: + funding_cursor_ts: int, venue: str = "") -> dict[str, Any]: return {"positions": {s: float(q) for s, q in positions.items()}, "marks": {s: float(m) for s, m in marks.items()}, - "envelope": float(envelope), "funding_cursor_ts": int(funding_cursor_ts)} + "envelope": float(envelope), "funding_cursor_ts": int(funding_cursor_ts), + "venue": str(venue)} def compute_crypto_exec_return(pos_prior: dict[str, Any], today_marks: dict[str, float], @@ -76,11 +87,13 @@ class _CryptoExecBookingStrategy: def record_bybit_exec_track(repo: Any, exec_return: float | None, new_positions: dict[str, float], today_marks: dict[str, float], envelope: float, funding_cursor_ts: int, - today: str, at: dt.datetime) -> None: + today: str, at: dt.datetime, venue: str = "") -> None: """Book `exec_return` as an observed forward_record for `bybit_4edge_exec` via A's run_track (anchor + record + nav/summary), then carry the next run's basis (positions/marks/envelope/funding cursor) under the - book-state key `bybit_4edge_exec_pos` (SEPARATE from run_track's own book_state).""" + book-state key `bybit_4edge_exec_pos` (SEPARATE from run_track's own book_state). `venue` is recorded on + the exec book-state.""" from fxhnt.application.forward_engine import run_track run_track(repo, "bybit_4edge_exec", _CryptoExecBookingStrategy(exec_return), today=today, at=at) repo.set_book_state("bybit_4edge_exec_pos", - build_crypto_pos_state(new_positions, today_marks, envelope, funding_cursor_ts), at=at) + build_crypto_pos_state(new_positions, today_marks, envelope, funding_cursor_ts, venue), + at=at) diff --git a/tests/unit/test_bybit_exec_record.py b/tests/unit/test_bybit_exec_record.py index 2bd97c3..c186a0e 100644 --- a/tests/unit/test_bybit_exec_record.py +++ b/tests/unit/test_bybit_exec_record.py @@ -97,3 +97,32 @@ def test_apply_fills_rejects_unknown_side(): fills = [{"symbol": "SOLUSDT", "side": "HOLD", "qty": 2.0}] with pytest.raises(ValueError, match="apply_fills: unexpected fill side"): apply_fills(current, fills) + + +def test_build_crypto_pos_state_persists_venue(): + from fxhnt.application.bybit_exec_record import build_crypto_pos_state + st = build_crypto_pos_state({"BTCUSDT": 1.0}, {"BTCUSDT": 60000.0}, 5000.0, 123, venue="bybit-testnet") + assert st["venue"] == "bybit-testnet" and st["funding_cursor_ts"] == 123 + + +def test_paper_or_b2a_crypto_envelope_paper_mode(): + from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope + class _Repo: + def current_allocation(self): + raise AssertionError("B2a must not be read in paper mode") + assert paper_or_b2a_crypto_envelope(_Repo(), equity=8000.0, paper_envelope=10000.0, is_paper=True) == 8000.0 + + +def test_paper_or_b2a_crypto_envelope_refuses_non_testnet(): + import pytest + from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope + with pytest.raises(ValueError, match="paper-envelope refused"): + paper_or_b2a_crypto_envelope(object(), equity=8000.0, paper_envelope=10000.0, is_paper=False) + + +def test_paper_or_b2a_crypto_envelope_default_reads_b2a(): + from fxhnt.application.bybit_exec_record import paper_or_b2a_crypto_envelope + class _Repo: + def current_allocation(self): + return {"bybit_4edge": 3000.0} + assert paper_or_b2a_crypto_envelope(_Repo(), equity=8000.0, paper_envelope=0.0, is_paper=True) == 3000.0