-
Notifications
You must be signed in to change notification settings - Fork 0
Keep safe haven cash for unbuyable small accounts #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,7 @@ class ExecutionCycleResult: | |
|
|
||
|
|
||
| DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0 | ||
| SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0 | ||
|
|
||
|
|
||
| def _floor_quantity(quantity: float) -> int: | ||
|
|
@@ -90,6 +91,16 @@ def _safe_haven_cash_symbols(*, portfolio: dict[str, Any], allocation: dict[str, | |
| return tuple(dict.fromkeys(symbols)) | ||
|
|
||
|
|
||
| def _positive_target_total(targets: dict[str, Any]) -> float: | ||
| total = 0.0 | ||
| for value in dict(targets or {}).values(): | ||
| try: | ||
| total += max(0.0, float(value or 0.0)) | ||
| except (TypeError, ValueError): | ||
| continue | ||
| return total | ||
|
|
||
|
|
||
| def substitute_small_safe_haven_targets_with_cash( | ||
| plan: dict[str, Any], | ||
| *, | ||
|
|
@@ -134,17 +145,22 @@ def _apply_small_account_whole_share_compatibility( | |
| ) -> dict[str, Any]: | ||
| adjusted_plan = dict(plan or {}) | ||
| allocation = dict(adjusted_plan.get("allocation") or {}) | ||
| portfolio = dict(adjusted_plan.get("portfolio") or {}) | ||
| targets = dict(allocation.get("targets") or {}) | ||
| candidate_symbols = tuple( | ||
| dict.fromkeys( | ||
| tuple(allocation.get("risk_symbols", ())) | ||
| str(symbol or "").strip().upper() | ||
| for symbol in tuple(allocation.get("risk_symbols", ())) | ||
| + tuple(allocation.get("income_symbols", ())) | ||
| if str(symbol or "").strip() | ||
| ) | ||
| ) | ||
| if not candidate_symbols: | ||
| safe_haven_symbols = set(allocation.get("safe_haven_symbols", ())) | ||
| safe_haven_symbols = set(_safe_haven_cash_symbols(portfolio=portfolio, allocation=allocation)) | ||
| candidate_symbols = tuple( | ||
| symbol for symbol in targets if symbol not in safe_haven_symbols | ||
| str(symbol or "").strip().upper() | ||
| for symbol in targets | ||
| if str(symbol or "").strip().upper() not in safe_haven_symbols | ||
| ) | ||
| prices = {} | ||
| for symbol in candidate_symbols: | ||
|
|
@@ -157,9 +173,27 @@ def _apply_small_account_whole_share_compatibility( | |
| symbols=candidate_symbols, | ||
| quantity_step=1.0, | ||
| ) | ||
| safe_haven_symbols = _safe_haven_cash_symbols(portfolio=portfolio, allocation=allocation) | ||
| remaining_non_safe_targets = [ | ||
| symbol | ||
| for symbol in candidate_symbols | ||
| if float(adjusted_targets.get(str(symbol or "").strip().upper(), 0.0) or 0.0) > 0.0 | ||
| ] | ||
| safe_haven_substituted: list[str] = [] | ||
| if ( | ||
| substituted | ||
| and not remaining_non_safe_targets | ||
| and _positive_target_total(adjusted_targets) <= SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD | ||
| ): | ||
|
Comment on lines
+183
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| for symbol in safe_haven_symbols: | ||
| if float(adjusted_targets.get(symbol, 0.0) or 0.0) > 0.0: | ||
| adjusted_targets[symbol] = 0.0 | ||
| safe_haven_substituted.append(symbol) | ||
| allocation["targets"] = adjusted_targets | ||
| if substituted: | ||
| allocation["small_account_whole_share_substituted_symbols"] = substituted | ||
| if safe_haven_substituted: | ||
| allocation["small_account_safe_haven_cash_substituted_symbols"] = tuple(safe_haven_substituted) | ||
| adjusted_plan["allocation"] = allocation | ||
| return adjusted_plan | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
risk_symbols/income_symbolsarrive in a different case than the target keys, this lookup misses still-buyable non-safe targets afterproject_unbuyable_value_targets_to_cashnormalizes target keys. In a mixed case plan such asrisk_symbols=("soxl", "soxx")with uppercase targets where only SOXX is below one share, both lookups return zero here, so the code incorrectly treats all non-safe targets as gone and zeros the safe-haven target even though SOXL remains buyable. Normalizesymbolfor theadjusted_targets.get(...)check.Useful? React with 👍 / 👎.