-
Notifications
You must be signed in to change notification settings - Fork 0
Keep safe haven cash for unbuyable small accounts #95
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 |
|---|---|---|
|
|
@@ -125,6 +125,7 @@ class ExecutionCycleResult: | |
|
|
||
|
|
||
| DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0 | ||
| SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0 | ||
|
|
||
|
|
||
| def _noop_sleep(_seconds): | ||
|
|
@@ -143,6 +144,16 @@ def _safe_haven_cash_symbols(*, portfolio: dict, allocation: dict) -> tuple[str, | |
| return tuple(dict.fromkeys(symbols)) | ||
|
|
||
|
|
||
| def _positive_target_total(targets: dict) -> 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 _apply_safe_haven_cash_substitution( | ||
| *, | ||
| plan, | ||
|
|
@@ -266,14 +277,20 @@ def _apply_small_account_whole_share_compatibility( | |
| target_values = 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=dict((plan or {}).get("portfolio") or {}), allocation=allocation) | ||
| ) | ||
| candidate_symbols = tuple( | ||
| symbol for symbol in target_values if symbol not in safe_haven_symbols | ||
| str(symbol or "").strip().upper() | ||
| for symbol in target_values | ||
| if str(symbol or "").strip().upper() not in safe_haven_symbols | ||
| ) | ||
| quote_prices = {} | ||
| for symbol in candidate_symbols: | ||
|
|
@@ -289,11 +306,32 @@ def _apply_small_account_whole_share_compatibility( | |
| symbols=candidate_symbols, | ||
| quantity_step=1.0, | ||
| ) | ||
| safe_haven_symbols = _safe_haven_cash_symbols( | ||
| portfolio=dict((plan or {}).get("portfolio") or {}), | ||
| 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 | ||
| ): | ||
| for symbol in safe_haven_symbols: | ||
| if float(adjusted_targets.get(symbol, 0.0) or 0.0) > 0.0: | ||
| adjusted_targets[symbol] = 0.0 | ||
|
Comment on lines
+325
to
+326
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 👍 / 👎. |
||
| safe_haven_substituted.append(symbol) | ||
| adjusted_allocation = {**dict(allocation or {}), "targets": adjusted_targets} | ||
| if substituted: | ||
| adjusted_allocation["small_account_whole_share_substituted_symbols"] = substituted | ||
| if safe_haven_substituted: | ||
| adjusted_allocation["small_account_safe_haven_cash_substituted_symbols"] = tuple(safe_haven_substituted) | ||
| adjusted_plan = dict(plan or {}) | ||
| if substituted: | ||
| if substituted or safe_haven_substituted: | ||
| adjusted_plan["allocation"] = adjusted_allocation | ||
| return adjusted_plan, adjusted_allocation | ||
|
|
||
|
|
@@ -830,10 +868,17 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): | |
| **note_kwargs, | ||
| ) | ||
|
|
||
| cash_sweep_substituted_to_cash = bool( | ||
| allocation.get("small_account_safe_haven_cash_substituted_symbols") | ||
| ) | ||
| if ( | ||
| not cash_sweep_sold_this_cycle | ||
| and cash_sweep_symbol | ||
| and cash_sweep_symbol in strategy_assets | ||
| and ( | ||
| float(target_values.get(cash_sweep_symbol, 0.0) or 0.0) > 0.0 | ||
| or not cash_sweep_substituted_to_cash | ||
| ) | ||
| ): | ||
| cash_sweep_price = safe_quote_last_price( | ||
| f"{cash_sweep_symbol}.US", | ||
|
|
||
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.
When a risk/income target is projected to zero, this cap is checked against
adjusted_targets, so the substituted sleeve no longer contributes to the$2,000limit. For example, with a$1,900risk target whose share price is$2,000plus a$1,900BOXX target,substitutedis true and the adjusted total is only$1,900, causing BOXX to be zeroed and sold/blocked even though the strategy account was targeting$3,800; use the original target total or portfolio equity for the small-account limit.Useful? React with 👍 / 👎.