fix: a zero equity reading sized off the $100k default
Tradier returns total_equity 0.0 during its overnight maintenance window -- sixteen times in the last fourteen days, all around 01:06. Three defects compounded so that a zero could size a position off default_equity: account_equity = equity or config.default_equity `or` cannot tell "not supplied" from "read as zero", so a real 0.0 became 100000. Then the position-value cap, derived from the same equity, was itself 0 -- and the guard if max_position_value is not None and max_position_value > 0: SKIPPED the cap precisely when the cap said "no room". Finally the auto path (~96% of approval requests) had no `pos_cap <= 0` early-out, unlike the manual path at request_entry_approval. Net effect on a ~$250 account: risk 5% of 100k = $5,000, capped only by liquidity. The broker would have rejected it for buying power, which is luck rather than design. Never fired in live hours -- every zero read landed with the market closed, and _current_equity caches the last good value -- so this is latent, not a realised loss. The reachable path is a boot that coincides with the maintenance window, when no good read has been cached yet. Distinguish None from zero, refuse to size on non-positive equity, treat a zero cap as "no room" rather than "no cap", and mirror the manual path's early-out. TRADING_DEFAULT_EQUITY is set to 300 on the live stack so even the untested fallback cannot oversize. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2LepdJfPCfK5Q8uznjFBM
This commit is contained in:
1 parent
0910e29cc5
commit
9034839661
4 files changed
+67
-2
No files matched your search
@@ -68,3 +68,46 @@ def test_rank_risk_factor_concentrates():
|
||||
assert rank_risk_factor(2, 2) == 0.5 # rank-2 half
|
||||
assert rank_risk_factor(3, 2) == 0.0 # outside top-2 -> skip
|
||||
assert rank_risk_factor(0, 2) == 1.0 # unranked -> not concentrated
|
||||
|
||||
|
||||
# --- zero-equity guards (2026-09-13) -----------------------------------------
|
||||
# Tradier returned total_equity 0.0 sixteen times in fourteen days, all inside
|
||||
# its ~01:06 maintenance window. `equity or default` read that real zero as
|
||||
# "not supplied" and sized off default_equity (100k) — on a $241 account that
|
||||
# is a position risking 5% of 100k. Two independent holes made it reachable:
|
||||
# the falsy-zero fallback, and a `max_position_value > 0` test that SKIPPED the
|
||||
# cap exactly when the cap was zero.
|
||||
|
||||
def test_zero_equity_refuses_to_size():
|
||||
"""A real 0.0 reading must size nothing — never fall back to default_equity."""
|
||||
r = calculate_position_size(
|
||||
4.0, 3.80, equity=0.0,
|
||||
config=PositionSizingConfig(risk_per_trade_pct=0.05, default_equity=100000),
|
||||
)
|
||||
assert r.position_size == 0
|
||||
assert r.dollar_amount == 0.0
|
||||
|
||||
|
||||
def test_negative_equity_refuses_to_size():
|
||||
r = calculate_position_size(4.0, 3.80, equity=-5.0, config=CFG)
|
||||
assert r.position_size == 0
|
||||
|
||||
|
||||
def test_none_equity_still_uses_the_default():
|
||||
"""The documented fallback must survive: not-passed is not the same as zero."""
|
||||
r = calculate_position_size(
|
||||
4.0, 3.80, equity=None,
|
||||
config=PositionSizingConfig(risk_per_trade_pct=0.01, default_equity=100000),
|
||||
)
|
||||
assert r.position_size > 0
|
||||
|
||||
|
||||
def test_zero_position_cap_blocks_the_trade():
|
||||
"""A cap of 0 means no room (no settled cash / gross budget spent), not 'no cap'."""
|
||||
r = calculate_position_size(4.0, 3.80, equity=300, config=CFG, max_position_value=0.0)
|
||||
assert r.position_size == 0
|
||||
|
||||
|
||||
def test_positive_cap_still_caps():
|
||||
r = calculate_position_size(4.0, 3.80, equity=300, config=CFG, max_position_value=40.0)
|
||||
assert r.position_size == 10 # 40 / 4.00
|
||||
Reference in new issue
Block a user