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