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>
88 lines
3.8 KiB
Python
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)
|