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
4 changes: 3 additions & 1 deletion src/us_equity_strategies/strategies/tqqq_growth_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ def build_rebalance_plan(
)
sell_order_symbols = ("TQQQ", unlevered_symbol, "SPYI", "QQQI", "BOXX")
buy_order_symbols = ("SPYI", "QQQI", "TQQQ", unlevered_symbol)
snapshot_metadata = getattr(snapshot, "metadata", {}) or {}
account_hash = snapshot_metadata.get("account_hash") if isinstance(snapshot_metadata, dict) else None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve account_hash from any Mapping metadata

The new extraction only reads account_hash when snapshot.metadata is a concrete dict, so snapshots that provide metadata as another Mapping type (e.g., MappingProxyType or custom mapping wrappers) will now silently lose account_hash even when the key is present. This is a regression from the prior snapshot.metadata["account_hash"] behavior and can break downstream execution metadata routing for environments that pass non-dict mappings; elsewhere in this repo metadata is normalized via isinstance(..., Mapping) (for example _metadata_mapping in entrypoints/_portfolio_dashboard.py), so this narrower type check is inconsistent.

Useful? React with 👍 / 👎.


return {
"strategy_symbols": strategy_symbols,
Expand All @@ -240,7 +242,7 @@ def build_rebalance_plan(
"buy_order_symbols": buy_order_symbols,
"cash_sweep_symbol": "BOXX",
"portfolio_rows": (("TQQQ", unlevered_symbol, "BOXX"), ("QQQI", "SPYI")),
"account_hash": snapshot.metadata["account_hash"],
"account_hash": account_hash,
"market_values": market_values,
"quantities": quantities,
"total_equity": total_equity,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_strategy_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def test_tqqq_growth_income_exposes_live_dual_drive_metadata(self):
self.assertEqual(plan["sell_order_symbols"], ("TQQQ", "QQQ", "SPYI", "QQQI", "BOXX"))
self.assertEqual(plan["buy_order_symbols"], ("SPYI", "QQQI", "TQQQ", "QQQ"))
self.assertEqual(plan["portfolio_rows"], (("TQQQ", "QQQ", "BOXX"), ("QQQI", "SPYI")))
self.assertEqual(plan["account_hash"], "acct-1")
self.assertEqual(plan["allocation_mode"], "fixed_qqq_tqqq_pullback")
self.assertAlmostEqual(plan["target_values"]["TQQQ"], 150000.0 * 0.45)
self.assertAlmostEqual(plan["target_values"]["QQQ"], 150000.0 * 0.45)
Expand All @@ -80,6 +81,41 @@ def test_tqqq_growth_income_exposes_live_dual_drive_metadata(self):
self.assertEqual(plan["notification_context"]["portfolio"]["reserved_cash"], 15000.0)
self.assertEqual(plan["notification_context"]["portfolio"]["investable_cash"], 5000.0)

def test_tqqq_growth_income_accepts_portfolio_without_account_hash(self):
_skip_if_missing_numeric_stack()
from us_equity_strategies.strategies.tqqq_growth_income import (
build_rebalance_plan as build_tqqq_plan,
)

qqq_history = [
{"close": 100.0 + index * 0.5, "high": 101.0 + index * 0.5, "low": 99.0 + index * 0.5}
for index in range(260)
]
snapshot = SimpleNamespace(
positions=[SimpleNamespace(symbol="BOXX", market_value=150000.0, quantity=1000)],
total_equity=150000.0,
buying_power=20000.0,
metadata={"account_ids": ["U18308207"]},
)

plan = build_tqqq_plan(
qqq_history,
snapshot,
signal_text_fn=lambda icon: icon,
translator=_translator,
income_threshold_usd=1_000_000_000.0,
qqqi_income_ratio=0.5,
cash_reserve_ratio=0.03,
rebalance_threshold_ratio=0.01,
dual_drive_qqq_weight=0.45,
dual_drive_tqqq_weight=0.45,
dual_drive_cash_reserve_ratio=0.10,
)

self.assertIsNone(plan["account_hash"])
self.assertEqual(plan["sell_order_symbols"], ("TQQQ", "QQQ", "SPYI", "QQQI", "BOXX"))
self.assertEqual(plan["notification_context"]["portfolio"]["raw_buying_power"], 20000.0)

def test_tqqq_growth_income_can_trade_qqqm_while_using_qqq_signal(self):
_skip_if_missing_numeric_stack()
from us_equity_strategies.strategies.tqqq_growth_income import (
Expand Down