129 lines
5.7 KiB
Python
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
|