Files
fxhnt/tests/unit/test_bybit_exec_record.py

129 lines
5.7 KiB
Python

from fxhnt.application.bybit_exec_record import (
build_crypto_pos_state,
compute_crypto_exec_return,
crypto_envelope,
)
def _state(positions, marks, envelope, cursor=0):
return build_crypto_pos_state(positions, marks, envelope, cursor)
def test_inception_and_flat_return_none():
assert compute_crypto_exec_return({}, {"BTCUSDT": 100.0}, 0.0, 0.0) is None # no prior book
flat = _state({"BTCUSDT": 1.0}, {"BTCUSDT": 100.0}, 0.0) # envelope 0
assert compute_crypto_exec_return(flat, {"BTCUSDT": 100.0}, 0.0, 0.0) is None
def test_envelope_basis_undiluted():
# $10k envelope, book long 100 units entered at 100 -> mark 101 = +$100 price pnl. Return = 100/10000 = 1%,
# regardless of any (irrelevant) larger testnet balance — the divisor is the ENVELOPE.
prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 0.0, 0.0)
assert abs(r - 0.01) < 1e-9
def test_funding_is_return_not_flow():
# flat marks, +$50 funding on a $10k envelope -> +0.5% (funding is edge return).
prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 50.0, 0.0)
assert abs(r - 0.005) < 1e-9
def test_fees_reduce_return():
# flat marks, $30 fees on a $10k envelope -> -0.3%.
prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
r = compute_crypto_exec_return(prior, {"BTCUSDT": 100.0}, 0.0, 30.0)
assert abs(r - (-0.003)) < 1e-9
def test_missing_today_mark_falls_back_to_prior():
prior = _state({"BTCUSDT": 100.0}, {"BTCUSDT": 100.0}, 10_000.0)
assert compute_crypto_exec_return(prior, {}, 0.0, 0.0) == 0.0 # no price move
def test_missing_prior_mark_is_zero_pnl_not_full_notional():
# a held symbol absent from PRIOR marks (rotated out then back) must NOT book its full notional as gain.
prior = _state({"BTCUSDT": 100.0}, {}, 10_000.0) # no prior mark stored
r = compute_crypto_exec_return(prior, {"BTCUSDT": 101.0}, 0.0, 0.0)
assert r == 0.0 # NOT +101*100/10000
class _Repo:
def __init__(self, alloc):
self._alloc = alloc
def current_allocation(self):
return self._alloc
def test_crypto_envelope_caps_at_equity_and_zero_when_absent():
assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 5_000.0) == 5_000.0 # capped at equity
assert crypto_envelope(_Repo({"bybit_4edge": 8_000.0}), 20_000.0) == 8_000.0 # alloc binds
assert crypto_envelope(_Repo({}), 20_000.0) == 0.0 # de-funded -> flatten
def test_apply_fills_builds_post_rebalance_book():
from fxhnt.application.bybit_exec_record import apply_fills
current = {"BTCUSDT": 1.0, "ETHUSDT": -2.0}
fills = [{"symbol": "BTCUSDT", "side": "BUY", "qty": 0.5}, # 1.0 + 0.5 = 1.5
{"symbol": "ETHUSDT", "side": "BUY", "qty": 2.0}, # -2.0 + 2.0 = 0.0 -> dropped
{"symbol": "SOLUSDT", "side": "SELL", "qty": 3.0}, # new short -3.0
{"symbol": "BTCUSDT", "error": True, "qty": 9.0}] # error row ignored
assert apply_fills(current, fills) == {"BTCUSDT": 1.5, "SOLUSDT": -3.0}
def test_apply_fills_case_insensitive_side():
"""Lowercase and mixed-case sides should be normalized and classified correctly."""
from fxhnt.application.bybit_exec_record import apply_fills
# lowercase "sell" should result in negative signed qty
current = {"ETHUSDT": 1.0}
fills = [{"symbol": "ETHUSDT", "side": "sell", "qty": 1.0}] # 1.0 + (-1.0) = 0.0 -> dropped
assert apply_fills(current, fills) == {}
# mixed-case "BuY" should result in positive signed qty
current = {"BTCUSDT": 0.0}
fills = [{"symbol": "BTCUSDT", "side": "BuY", "qty": 1.5}] # 0.0 + 1.5 = 1.5
assert apply_fills(current, fills) == {"BTCUSDT": 1.5}
def test_apply_fills_rejects_unknown_side():
"""Unknown side values (e.g. 'HOLD') should raise ValueError."""
import pytest
from fxhnt.application.bybit_exec_record import apply_fills
current = {"SOLUSDT": 0.0}
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