Files
fxhnt/tests/unit/test_bybit_testnet_execution.py
jgrusewski 421317ae00 feat(exec): bybit testnet diff order planning (no overlay re-apply)
plan_orders diffs the raw bybit_4edge sleeve weights straight against
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. Frozen DTOs
OrderIntent / InstrumentLimit (stable contract for Tasks 5/8). Pure.

Task-1 gate confirmed bybit_4edge is naive equal-weight with NO risk
overlay (see bybit_paper_book docstring + latest_raw_sleeve_weights), so
the design's effective_weights/overlay-reapply is dropped — the raw
weights ARE the effective weights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:07:39 +02:00

74 lines
3.0 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 == []