Files
fxhnt/tests/unit/test_bybit_testnet_execution.py
jgrusewski 110d1bd3b7 fix(bybit): missing-prior-mark 0-pnl (not full notional) + funding-cursor precision + flatten test (C2 final review)
Critical: a held symbol absent from PRIOR marks (rotated out of the crypto
universe then back in) was booking its FULL notional as P&L in
compute_crypto_exec_return, since a missing prior mark fell back to price 0.
Fix falls back to today's mark instead, so a missing prior mark yields 0 P&L
for that symbol this period. Also: document the accepted re-entry-day fee
gap, advance the funding cursor to the reconcile's funding-query timestamp
(no gap to the next period's funding_since), and strengthen the flatten
test to use a nonzero sleeve weight with equity=0, pinning that equity=0
alone flattens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:06:37 +02:00

88 lines
3.8 KiB
Python

"""Order planning for the Bybit testnet real-paper leg (design §4.2).
NO overlay re-apply: `bybit_4edge` is built naive equal-weight with NO risk overlay (see
`bybit_paper_book` module docstring + `latest_raw_sleeve_weights`), so the raw sleeve weights ARE the
effective weights — there is nothing to re-apply. `plan_orders` therefore diffs the raw target weights
straight against the testnet positions: target_qty = weight·equity/mark snapped to qtyStep, dust-filtered on
minNotional/minOrderQty, delta vs current, reduce_only on reductions/closes, gross capped at max_gross=1.0.
"""
from __future__ import annotations
from fxhnt.application.bybit_testnet_execution import (
InstrumentLimit,
OrderIntent,
plan_orders,
)
def _lim(min_order_qty: float = 0.001, qty_step: float = 0.001,
min_notional: float = 10.0) -> dict[str, InstrumentLimit]:
return {
"BTCUSDT": InstrumentLimit(min_order_qty, qty_step, min_notional),
"ETHUSDT": InstrumentLimit(min_order_qty, qty_step, min_notional),
"X": InstrumentLimit(min_order_qty, qty_step, min_notional),
"A": InstrumentLimit(min_order_qty, qty_step, min_notional),
"B": InstrumentLimit(min_order_qty, 0.5, min_notional),
}
def test_buy_when_target_above_current():
intents = plan_orders({"BTCUSDT": 0.5}, {"BTCUSDT": 0.0}, equity=10_000,
marks={"BTCUSDT": 100.0}, limits=_lim())
assert intents == [OrderIntent("BTCUSDT", qty=50.0, reduce_only=False)] # 0.5*10000/100
def test_reduce_only_on_close():
intents = plan_orders({"BTCUSDT": 0.0}, {"BTCUSDT": 50.0}, 10_000,
{"BTCUSDT": 100.0}, _lim())
assert intents[0].reduce_only is True
assert intents[0].qty == -50.0
def test_partial_reduce_is_reduce_only():
intents = plan_orders({"BTCUSDT": 0.2}, {"BTCUSDT": 50.0}, 10_000,
{"BTCUSDT": 100.0}, _lim())
assert intents[0].qty == -30.0 # 20 target - 50 current
assert intents[0].reduce_only is True
def test_min_notional_filters_dust():
intents = plan_orders({"X": 0.0001}, {"X": 0.0}, 10_000, {"X": 100.0},
_lim(min_notional=50))
assert intents == [] # 0.0001*10000=1 USD < 50 minNotional
def test_qty_step_snap():
# 0.507*10000/100 = 50.7, snapped to a 0.5 step → 50.5
intents = plan_orders({"B": 0.507}, {"B": 0.0}, 10_000, {"B": 100.0}, _lim())
assert intents == [OrderIntent("B", qty=50.5, reduce_only=False)]
def test_gross_cap_clips_oversized_weights():
# Σ|weight·equity| = 1.6·equity > equity·1.0 → scale by 0.625 → 0.5 each → qty 50 each
intents = plan_orders({"A": 0.8, "B": 0.8}, {"A": 0.0, "B": 0.0}, 10_000,
{"A": 100.0, "B": 100.0}, _lim())
by_symbol = {i.symbol: i for i in intents}
assert by_symbol["A"].qty == 50.0
assert by_symbol["B"].qty == 50.0
def test_no_order_when_already_on_target():
intents = plan_orders({"BTCUSDT": 0.5}, {"BTCUSDT": 50.0}, 10_000,
{"BTCUSDT": 100.0}, _lim())
assert intents == []
def test_flatten_on_zero_equity_closes_held_position():
# De-funded / killswitched edge: crypto_envelope returns 0 -> equity=0 here, so target_qty collapses to
# 0 regardless of the target weight (pinned NONZERO below — the production flatten path passes LIVE sleeve
# weights, not a zeroed one), and the diff against the held long is a reduce_only close.
intents = plan_orders({"BTCUSDT": 0.5}, {"BTCUSDT": 1.0}, equity=0.0,
marks={"BTCUSDT": 100.0}, limits=_lim(min_notional=5.0),
min_order_usd=10.0, max_gross=1.0)
assert len(intents) == 1
intent = intents[0]
assert intent.symbol == "BTCUSDT"
assert intent.reduce_only is True
assert intent.qty == -1.0 # closes the entire long (target_qty collapses to 0 despite nonzero weight)