Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions application/ibkr_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,20 @@ def fetch_portfolio_snapshot(
if verified_nlv is not None and source_digest is not None:
metadata["broker_net_liquidation"] = float(verified_nlv)
metadata["source_digest_sha256"] = source_digest
# Cash-only SOXL sizes value targets from positions+cash, while RRL
# divides by capital_base NLV. When sleeve marks exceed NetLiquidation,
# in-cap weights inflate and fail closed. Shrink the cash sleeve so
# strategy equity matches the verified USD NLV used by the gate.
if cash_only_execution and market_currency == "USD":
position_mv_sum = sum(float(position.market_value) for position in positions)
strategy_equity = float(total_equity)
nlv = float(verified_nlv)
if strategy_equity > nlv + 1e-6:
metadata["strategy_equity_before_nlv_align"] = strategy_equity
aligned_cash = nlv - position_mv_sum
metadata["market_currency_cash"] = aligned_cash
total_equity = nlv
buying_power = aligned_cash

return PortfolioSnapshot(
as_of=datetime.now(timezone.utc),
Expand Down
27 changes: 26 additions & 1 deletion strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,31 @@ def _evaluate_value_target_strategy(
),
)
decision = self.entrypoint.evaluate(ctx)
capability_status = self._capability_status_metadata(portfolio_snapshot)
if any(str(flag).startswith("rejected:runtime_risk") for flag in decision.risk_flags):
diagnostics = decision.diagnostics if isinstance(decision.diagnostics, Mapping) else {}
snapshot_metadata = getattr(portfolio_snapshot, "metadata", None)
broker_nlv = (
snapshot_metadata.get("broker_net_liquidation")
if isinstance(snapshot_metadata, Mapping)
else None
)
target_value_sum = sum(
float(position.target_value)
for position in decision.positions
if position.target_value is not None
)
self.logger(
"strategy_runtime_risk_reject | "
f"profile={self.profile} "
f"capital_base_status={capability_status.get('capital_base_status')} "
f"runtime_risk_status={capability_status.get('runtime_risk_status')} "
f"risk_flags={','.join(str(flag) for flag in decision.risk_flags)} "
f"hold={diagnostics.get('runtime_risk_small_account_hold')} "
f"broker_nlv={broker_nlv} "
f"portfolio_total_equity={getattr(portfolio_snapshot, 'total_equity', None)} "
f"target_value_sum={target_value_sum}"
)
safe_haven_symbol = next(
(position.symbol for position in decision.positions if position.role == "safe_haven"),
None,
Expand All @@ -1340,7 +1365,7 @@ def _evaluate_value_target_strategy(
metadata = self._enrich_portfolio_metadata(
{
"strategy_profile": self.profile,
**self._capability_status_metadata(portfolio_snapshot),
**capability_status,
"managed_symbols": managed_symbols,
"status_icon": self.status_icon,
"dry_run_only": self.runtime_settings.dry_run_only,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_ibkr_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,47 @@ def accountValues(self):

assert snapshot.metadata["total_equity_source"] == "broker_net_liquidation"
assert snapshot.metadata["broker_net_liquidation"] == 371.93
assert snapshot.total_equity == 371.93
assert snapshot.metadata["account_hash"] == "U15998061"
assert isinstance(snapshot.metadata["source_digest_sha256"], str)
assert len(snapshot.metadata["source_digest_sha256"]) == 64


def test_fetch_portfolio_snapshot_aligns_cash_only_equity_to_verified_usd_nlv():
class DriftedMarksIB(FakeIB):
def positions(self):
return [
SimpleNamespace(
account="U15998061",
contract=SimpleNamespace(secType="STK", symbol="SOXL", currency="USD"),
position=3,
avgCost=150.0,
)
]

def accountValues(self):
return [
SimpleNamespace(account="U15998061", currency="USD", tag="NetLiquidation", value="472.0"),
SimpleNamespace(account="U15998061", currency="USD", tag="CashBalance", value="40.0"),
SimpleNamespace(account="U15998061", currency="USD", tag="AvailableFunds", value="40.0"),
]

snapshot = fetch_portfolio_snapshot(
DriftedMarksIB(),
account_ids=("U15998061",),
wait_seconds=0,
currency="USD",
cash_only_execution=True,
)

# Position marks sum to 450 + cash 40 = 490 before align; gate uses NLV 472.
assert snapshot.metadata["broker_net_liquidation"] == 472.0
assert snapshot.metadata["strategy_equity_before_nlv_align"] == 490.0
assert snapshot.total_equity == 472.0
assert snapshot.metadata["market_currency_cash"] == 22.0
assert snapshot.buying_power == 22.0


def test_fetch_portfolio_snapshot_prefers_usd_net_liquidation_over_base():
class DualNetLiquidationIB(FakeIB):
def positions(self):
Expand Down