Files
fxhnt/tests/unit/test_vrp_exec_helpers.py
jgrusewski c4fb3f73eb fix(vrp): exec-twin ladder cap + per-rung sizing + combo BUY-credit sign + backfill master branch + P&L helper tests
The VRP exec twin previously opened a NEW full-envelope spread every run, overwriting the
single-slot book-state and never closing the prior one -- armed weekly it would stack N x
notional. plan_and_record_vrp now maintains a 5-rung ladder (mirrors VrpStrategy.advance):
closes rungs at 50% profit-take or DTE<=1 first, then opens at most one new rung sized at
envelope/ladder (never the full envelope) only when a slot is free. Ladder decisions are
factored into a pure, unit-tested plan_vrp_ladder helper.

Also: IbkrBroker.place_combo used the wrong parent-order action for a net-credit combo (SELL
with a negative limit); switched to BUY with a negative limit per IBKR's combo convention.
fxhnt-opra-backfill.yaml's git-sync cloned the feature branch instead of master (this Job is
applied manually post-merge). Added tests/unit/test_vrp_exec_helpers.py for the previously
untested pure P&L helpers (compute_spread_exec_return/compute_ladder_exec_return/build_pos_state).
Removed the dead --live flag from execute-vrp (plan_and_record_vrp never consulted allow_live;
the paper-envelope refusal guard is the real safety mechanism).

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

81 lines
3.6 KiB
Python

import pytest
from fxhnt.application.vrp_exec_record import (
build_pos_state,
compute_ladder_exec_return,
compute_spread_exec_return,
)
def test_compute_spread_exec_return_envelope_zero_is_none():
assert compute_spread_exec_return(prior_val=1.0, mark_today=0.8, qty=2, envelope=0.0) is None
def test_compute_spread_exec_return_no_mark_today_is_hold_not_phantom_zero():
assert compute_spread_exec_return(prior_val=1.0, mark_today=None, qty=2, envelope=200_000.0) is None
def test_compute_spread_exec_return_winning_spread_is_positive_credit_decay():
# short-vol: credit decayed from 1.00 -> 0.60 = profit. qty=2, $100/contract multiplier.
r = compute_spread_exec_return(prior_val=1.00, mark_today=0.60, qty=2, envelope=200_000.0)
assert r == pytest.approx((1.00 - 0.60) * 100.0 * 2 / 200_000.0)
assert r > 0.0
def test_compute_spread_exec_return_losing_spread_is_negative_and_bounded_by_width():
# a defined-risk put-credit-spread's worst mark is bounded by the wing width ($5 here = $500/contract);
# the value rising toward that cap is a loss, correctly signed negative.
r = compute_spread_exec_return(prior_val=1.00, mark_today=4.50, qty=2, envelope=200_000.0)
assert r == pytest.approx((1.00 - 4.50) * 100.0 * 2 / 200_000.0)
assert r < 0.0
def test_compute_ladder_exec_return_inception_no_prior_rungs_is_none():
assert compute_ladder_exec_return([], {}, 200_000.0) is None
def test_compute_ladder_exec_return_definanced_envelope_is_none():
prior = [{"short_osi": "S1", "long_osi": "L1", "credit": 1.0, "last_val": 1.0, "qty": 2}]
assert compute_ladder_exec_return(prior, {"S1": 0.5}, 0.0) is None
def test_compute_ladder_exec_return_sums_only_marked_rungs():
prior = [
{"short_osi": "S1", "long_osi": "L1", "credit": 1.0, "last_val": 1.0, "qty": 2},
{"short_osi": "S2", "long_osi": "L2", "credit": 1.0, "last_val": 1.0, "qty": 2},
]
marks = {"S1": 0.60, "S2": None} # S2 has no mark today -> HOLD, contributes 0 (not a phantom zero)
r = compute_ladder_exec_return(prior, marks, 200_000.0)
assert r == pytest.approx((1.00 - 0.60) * 100.0 * 2 / 200_000.0) # only S1's marked pnl
def test_compute_ladder_exec_return_all_holds_is_none():
prior = [{"short_osi": "S1", "long_osi": "L1", "credit": 1.0, "last_val": 1.0, "qty": 2}]
assert compute_ladder_exec_return(prior, {"S1": None}, 200_000.0) is None
def test_build_pos_state_round_trip():
opens = [{"entry": "2026-07-06", "expiry": "2026-08-15", "short_osi": "S1", "long_osi": "L1",
"credit": 1.0, "last_val": 0.6, "qty": 2}]
st = build_pos_state(opens, envelope=200_000.0, venue="ibkr-paper", today="2026-07-13")
assert st["open_spreads"] == opens
assert st["envelope"] == 200_000.0
assert st["venue"] == "ibkr-paper"
assert st["last_run_date"] == "2026-07-13"
def test_build_pos_state_defensively_copies_rungs():
opens = [{"entry": "2026-07-06", "expiry": "2026-08-15", "short_osi": "S1", "long_osi": "L1",
"credit": 1.0, "last_val": 0.6, "qty": 2}]
st = build_pos_state(opens, envelope=200_000.0, venue="ibkr-paper", today="2026-07-13")
opens.append({"short_osi": "S2"}) # mutate the caller's list after the call
opens[0]["last_val"] = 999.0 # mutate a rung dict in the caller's list
assert len(st["open_spreads"]) == 1
assert st["open_spreads"][0]["last_val"] == 0.6
def test_build_pos_state_empty_ladder():
st = build_pos_state([], envelope=0.0, venue="", today="2026-07-13")
assert st["open_spreads"] == []
assert st["envelope"] == 0.0