121 lines
5.3 KiB
Python
121 lines
5.3 KiB
Python
from fxhnt.application.multistrat_exec_record import build_pos_state, compute_exec_return, scaled_weights
|
|
|
|
|
|
def test_scaled_weights_bridge_and_flatten():
|
|
w = {"SPY": 0.6, "IEF": 0.4}
|
|
# envelope 10k in a 100k account → scale 0.1 → weights sum to envelope/NLV
|
|
s = scaled_weights(w, envelope=10_000.0, nlv=100_000.0)
|
|
assert abs(s["SPY"] - 0.06) < 1e-12 and abs(s["IEF"] - 0.04) < 1e-12
|
|
assert scaled_weights(w, 0.0, 100_000.0) == {"SPY": 0.0, "IEF": 0.0} # $0 envelope → flatten
|
|
assert scaled_weights(w, 10_000.0, 0.0) == {"SPY": 0.0, "IEF": 0.0} # no NLV → flatten (safe)
|
|
|
|
|
|
def test_compute_exec_return_first_run_is_none():
|
|
assert compute_exec_return({}, {"SPY": 500.0}) is None # no prior book → inception
|
|
|
|
|
|
def test_exec_return_is_envelope_basis_not_full_nlv():
|
|
# prior book: $10k envelope fully deployed in 20 SPY @ 500 (=$10k), in a (hypothetical) large account.
|
|
prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
|
|
# SPY +10% today → book +$1000 on the $10k envelope → 10% return (NOT diluted by any idle account cash)
|
|
assert abs(compute_exec_return(prior, {"SPY": 550.0}) - 0.10) < 1e-9
|
|
|
|
|
|
def test_reallocation_capital_flow_is_not_return():
|
|
# flat prices; the envelope will grow at the NEXT rebalance (a flow) — the RETURN of the prior book is ~0
|
|
prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
|
|
assert abs(compute_exec_return(prior, {"SPY": 500.0})) < 1e-12 # flat prices → 0%, not a jump
|
|
|
|
|
|
def test_partial_deployment_reserve_earns_zero():
|
|
# $10k envelope, only $6k deployed (12 SPY @ 500), $4k reserve. SPY +10% → +$600 on $10k = 6%.
|
|
prior = build_pos_state({"SPY": 12.0}, {"SPY": 500.0}, envelope=10_000.0)
|
|
assert abs(compute_exec_return(prior, {"SPY": 550.0}) - 0.06) < 1e-9
|
|
|
|
|
|
def test_none_prior_returns_none():
|
|
# None prior → inception, no AttributeError on .get() call
|
|
assert compute_exec_return(None, {"SPY": 500.0}) is None
|
|
|
|
|
|
def test_missing_price_degrades_not_raises():
|
|
# Position absent from today's prices uses prior price; no KeyError raised
|
|
prior = build_pos_state({"SPY": 20.0}, {"SPY": 500.0}, envelope=10_000.0)
|
|
assert compute_exec_return(prior, {}) == 0.0
|
|
|
|
|
|
def test_build_pos_state_persists_venue():
|
|
from fxhnt.application.multistrat_exec_record import build_pos_state
|
|
st = build_pos_state({"SPY": 10.0}, {"SPY": 5.0}, 1000.0, venue="alpaca-paper")
|
|
assert st["venue"] == "alpaca-paper" and st["envelope"] == 1000.0
|
|
|
|
|
|
def test_build_pos_state_venue_defaults_empty():
|
|
from fxhnt.application.multistrat_exec_record import build_pos_state
|
|
assert build_pos_state({}, {}, 0.0)["venue"] == ""
|
|
|
|
|
|
class _FakeExecSvc:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def rebalance_weights(self, sid, weights, prices, *, max_gross, execute):
|
|
self.calls.append((sid, dict(weights), execute))
|
|
class _Plan:
|
|
nlv = 100000.0; orders = []; notes = []; blocked = False; executed = execute
|
|
return _Plan()
|
|
|
|
|
|
class _FakeRepo:
|
|
def __init__(self, alloc):
|
|
self._alloc = alloc; self.book = {}
|
|
|
|
def current_allocation(self): # B2a — must NOT be called in paper mode
|
|
raise AssertionError("current_allocation must not be read in paper mode")
|
|
|
|
def get_book_state(self, k):
|
|
return self.book.get(k, {})
|
|
|
|
def set_book_state(self, k, extra, at):
|
|
self.book[k] = extra
|
|
|
|
|
|
class _B2aRepo(_FakeRepo):
|
|
def current_allocation(self):
|
|
return self._alloc
|
|
|
|
|
|
def test_plan_and_record_paper_mode_bypasses_b2a():
|
|
import datetime as dt
|
|
from fxhnt.application.multistrat_exec_record import plan_and_record
|
|
repo = _FakeRepo(alloc={"multistrat": 0.0}); svc = _FakeExecSvc()
|
|
_plan, envelope = plan_and_record(
|
|
repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
|
|
nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
|
|
execute=False, paper_envelope=1_000_000.0, account_is_paper=True, venue="alpaca-paper")
|
|
assert envelope == 500000.0 # min(1e6 paper, 5e5 nlv); B2a NOT consulted
|
|
|
|
|
|
def test_plan_and_record_paper_mode_refuses_live_account():
|
|
import datetime as dt
|
|
import pytest
|
|
from fxhnt.application.multistrat_exec_record import plan_and_record
|
|
repo = _FakeRepo(alloc={"multistrat": 0.0}); svc = _FakeExecSvc()
|
|
with pytest.raises(ValueError, match="paper-envelope refused"):
|
|
plan_and_record(
|
|
repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
|
|
nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
|
|
execute=True, paper_envelope=1_000_000.0, account_is_paper=False, venue="alpaca-live")
|
|
assert svc.calls == [] # NO orders on refusal
|
|
|
|
|
|
def test_plan_and_record_default_mode_reads_b2a():
|
|
import datetime as dt
|
|
from fxhnt.application.multistrat_exec_record import plan_and_record
|
|
repo = _B2aRepo(alloc={"multistrat": 25000.0}); svc = _FakeExecSvc()
|
|
_plan, envelope = plan_and_record(
|
|
repo=repo, exec_svc=svc, within_weights={"SPY": 1.0}, prices={"SPY": 10.0},
|
|
nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
|
|
execute=False, paper_envelope=0.0, account_is_paper=True)
|
|
assert envelope == 25000.0 # min($25k B2a, $500k nlv)
|