Skip to content

Commit 34caaf8

Browse files
Pigbibicursoragent
andcommitted
Fix small-account cash reserve handling and platform execution parity.
Honor reserved cash when sizing post-sell buys on IBKR, sync small-account allocation drift and workflow fixes across trading platforms, and refresh stale wechat verification test fixtures for RecruitmentComplianceWatch. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1ba5145 commit 34caaf8

8 files changed

Lines changed: 445 additions & 23 deletions

File tree

.github/workflows/sync-cloud-run-env.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ jobs:
709709
main_time="${scheduler_config[1]}"
710710
probe_time="${scheduler_config[2]}"
711711
precheck_time="${scheduler_config[3]}"
712+
# Keep probe/precheck parsed for runtime-target schema parity; this step only syncs the main scheduler.
713+
: "${probe_time}" "${precheck_time}"
712714
713715
service_url="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" \
714716
--project="${GCP_PROJECT_ID}" \

application/execution_service.py

Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
try:
1111
from quant_platform_kit.common.small_account_compatibility import (
1212
apply_small_account_cash_compatibility,
13+
build_small_account_allocation_drift_notes,
1314
)
1415
except ImportError: # pragma: no cover - compatibility with older pinned shared wheels
1516
@dataclass(frozen=True)
@@ -123,6 +124,9 @@ def apply_small_account_cash_compatibility(
123124
cash_substitution_notes=tuple(notes),
124125
)
125126

127+
def build_small_account_allocation_drift_notes(**_kwargs):
128+
return ()
129+
126130
@dataclass(frozen=True)
127131
class ExecutionCycleResult:
128132
submitted_orders: tuple[dict[str, Any], ...]
@@ -134,6 +138,39 @@ class ExecutionCycleResult:
134138
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
135139
SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
136140
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset({"TQQQ", "SOXL"})
141+
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
142+
"SOXX": 0.90,
143+
}
144+
SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
145+
"TQQQ": 0.90,
146+
"SOXL": 0.90,
147+
"SOXX": 0.90,
148+
}
149+
150+
151+
def _limit_buy_premium_for_symbol(symbol, default_premium, premium_by_symbol=None) -> float:
152+
normalized_symbol = str(symbol or "").strip().upper()
153+
try:
154+
fallback = float(default_premium)
155+
except (TypeError, ValueError):
156+
fallback = 1.005
157+
if not isinstance(premium_by_symbol, dict):
158+
return fallback
159+
raw_value = premium_by_symbol.get(normalized_symbol)
160+
if raw_value is None:
161+
return fallback
162+
try:
163+
premium = float(raw_value)
164+
except (TypeError, ValueError):
165+
return fallback
166+
return premium if premium > 0.0 else fallback
167+
168+
169+
def _limit_buy_price(symbol, price, default_premium, premium_by_symbol=None) -> float:
170+
return round(
171+
float(price) * _limit_buy_premium_for_symbol(symbol, default_premium, premium_by_symbol),
172+
2,
173+
)
137174

138175

139176
def _floor_quantity(quantity: float) -> int:
@@ -171,6 +208,30 @@ def _safe_haven_cash_symbols(*, portfolio: dict[str, Any], allocation: dict[str,
171208
return tuple(dict.fromkeys(symbols))
172209

173210

211+
def _small_account_drift_reference_targets(
212+
allocation: dict[str, Any],
213+
*,
214+
portfolio: dict[str, Any] | None = None,
215+
) -> dict[str, float]:
216+
allocation = dict(allocation or {})
217+
targets = {
218+
str(symbol or "").strip().upper(): float(value or 0.0)
219+
for symbol, value in dict(allocation.get("targets") or {}).items()
220+
}
221+
candidate_symbols = tuple(
222+
dict.fromkeys(
223+
str(symbol or "").strip().upper()
224+
for symbol in tuple(allocation.get("risk_symbols", ()))
225+
+ tuple(allocation.get("income_symbols", ()))
226+
if str(symbol or "").strip()
227+
)
228+
)
229+
if not candidate_symbols:
230+
safe_haven_symbols = set(_safe_haven_cash_symbols(portfolio=dict(portfolio or {}), allocation=allocation))
231+
candidate_symbols = tuple(symbol for symbol in targets if symbol not in safe_haven_symbols)
232+
return {symbol: targets.get(symbol, 0.0) for symbol in candidate_symbols if symbol in targets}
233+
234+
174235
def _positive_target_total(targets: dict[str, Any]) -> float:
175236
total = 0.0
176237
for value in dict(targets or {}).values():
@@ -210,6 +271,35 @@ def substitute_small_safe_haven_targets_with_cash(
210271
return adjusted_plan
211272

212273

274+
def _should_retain_existing_whole_share(symbol, *, target_value, price) -> bool:
275+
normalized_symbol = str(symbol or "").strip().upper()
276+
if normalized_symbol in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS:
277+
return True
278+
279+
min_target_share_ratio = (
280+
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL.get(normalized_symbol)
281+
)
282+
if min_target_share_ratio is None:
283+
return False
284+
quote_price = max(0.0, float(price or 0.0))
285+
if quote_price <= 0.0:
286+
return False
287+
return max(0.0, float(target_value or 0.0)) >= quote_price * float(min_target_share_ratio)
288+
289+
290+
def _should_bootstrap_whole_share_buy(symbol, *, target_value, limit_price) -> bool:
291+
normalized_symbol = str(symbol or "").strip().upper()
292+
min_target_share_ratio = (
293+
SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL.get(normalized_symbol)
294+
)
295+
if min_target_share_ratio is None:
296+
return False
297+
effective_limit_price = max(0.0, float(limit_price or 0.0))
298+
if effective_limit_price <= 0.0:
299+
return False
300+
return max(0.0, float(target_value or 0.0)) >= effective_limit_price * float(min_target_share_ratio)
301+
302+
213303
def _quote_price(market_data_port: MarketDataPort, symbol: str) -> float | None:
214304
try:
215305
price = float(market_data_port.get_quote(symbol).last_price)
@@ -222,6 +312,8 @@ def _apply_small_account_whole_share_compatibility(
222312
plan: dict[str, Any],
223313
*,
224314
market_data_port: MarketDataPort,
315+
limit_buy_premium: float = 1.005,
316+
limit_buy_premium_by_symbol: dict[str, float] | None = None,
225317
) -> dict[str, Any]:
226318
adjusted_plan = dict(plan or {})
227319
allocation = dict(adjusted_plan.get("allocation") or {})
@@ -248,6 +340,7 @@ def _apply_small_account_whole_share_compatibility(
248340
if price is not None:
249341
prices[str(symbol).strip().upper()] = price
250342
retained_symbols = []
343+
bootstrap_symbols = []
251344
quantities = {
252345
str(symbol or "").strip().upper(): float(quantity or 0.0)
253346
for symbol, quantity in dict(portfolio.get("quantities") or {}).items()
@@ -257,13 +350,33 @@ def _apply_small_account_whole_share_compatibility(
257350
for symbol, value in targets.items()
258351
}
259352
for symbol in candidate_symbols:
260-
if symbol not in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS:
261-
continue
262353
target_value = max(0.0, float(compatibility_targets.get(symbol, 0.0) or 0.0))
263354
price = max(0.0, float(prices.get(symbol, 0.0) or 0.0))
355+
limit_price = (
356+
_limit_buy_price(symbol, price, limit_buy_premium, limit_buy_premium_by_symbol)
357+
if price > 0.0
358+
else 0.0
359+
)
360+
if not _should_retain_existing_whole_share(symbol, target_value=target_value, price=price):
361+
if (
362+
quantities.get(symbol, 0.0) <= 0.0
363+
and 0.0 < target_value < limit_price
364+
and _should_bootstrap_whole_share_buy(symbol, target_value=target_value, limit_price=limit_price)
365+
):
366+
compatibility_targets[symbol] = limit_price
367+
bootstrap_symbols.append(symbol)
368+
continue
264369
if price > 0.0 and 0.0 < target_value < price and quantities.get(symbol, 0.0) >= 1.0:
265370
compatibility_targets[symbol] = price
266371
retained_symbols.append(symbol)
372+
continue
373+
if (
374+
quantities.get(symbol, 0.0) <= 0.0
375+
and 0.0 < target_value < limit_price
376+
and _should_bootstrap_whole_share_buy(symbol, target_value=target_value, limit_price=limit_price)
377+
):
378+
compatibility_targets[symbol] = limit_price
379+
bootstrap_symbols.append(symbol)
267380
safe_haven_symbols = _safe_haven_cash_symbols(portfolio=portfolio, allocation=allocation)
268381
compatibility = apply_small_account_cash_compatibility(
269382
compatibility_targets,
@@ -285,6 +398,10 @@ def _apply_small_account_whole_share_compatibility(
285398
allocation["small_account_existing_whole_share_retained_symbols"] = tuple(
286399
dict.fromkeys(retained_symbols)
287400
)
401+
if bootstrap_symbols:
402+
allocation["small_account_whole_share_bootstrap_symbols"] = tuple(
403+
dict.fromkeys(bootstrap_symbols)
404+
)
288405
if compatibility.cash_substitution_notes:
289406
allocation["small_account_whole_share_cash_notes"] = tuple(compatibility.cash_substitution_notes)
290407
adjusted_plan["allocation"] = allocation
@@ -334,6 +451,7 @@ def execute_value_target_plan(
334451
dry_run_only: bool,
335452
limit_sell_discount: float = 0.995,
336453
limit_buy_premium: float = 1.005,
454+
limit_buy_premium_by_symbol: dict[str, float] | None = None,
337455
max_order_notional_usd: float | None = None,
338456
safe_haven_cash_substitute_threshold_usd: float = DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD,
339457
) -> ExecutionCycleResult:
@@ -342,9 +460,16 @@ def execute_value_target_plan(
342460
plan,
343461
threshold_usd=safe_haven_cash_substitute_threshold_usd,
344462
)
463+
plan_portfolio = dict(plan.get("portfolio") or {})
464+
small_account_reference_target_values = _small_account_drift_reference_targets(
465+
dict(plan.get("allocation") or {}),
466+
portfolio=plan_portfolio,
467+
)
345468
plan = _apply_small_account_whole_share_compatibility(
346469
plan,
347470
market_data_port=market_data_port,
471+
limit_buy_premium=limit_buy_premium,
472+
limit_buy_premium_by_symbol=limit_buy_premium_by_symbol,
348473
)
349474
allocation = dict(plan.get("allocation") or {})
350475
portfolio = dict(plan.get("portfolio") or {})
@@ -359,6 +484,10 @@ def execute_value_target_plan(
359484
str(k).upper(): float(v or 0.0)
360485
for k, v in dict(portfolio.get("sellable_quantities") or {}).items()
361486
}
487+
current_quantities = {
488+
str(k).upper(): float(v or 0.0)
489+
for k, v in dict(portfolio.get("quantities") or sellable_quantities).items()
490+
}
362491
threshold = float(
363492
execution.get("current_min_trade")
364493
or execution.get("trade_threshold_value")
@@ -376,6 +505,7 @@ def execute_value_target_plan(
376505

377506
submitted: list[dict[str, Any]] = []
378507
skipped: list[dict[str, Any]] = []
508+
reference_prices: dict[str, float] = {}
379509

380510
tradable_deltas: list[tuple[str, float, float]] = []
381511
for symbol in sorted(set(targets) | set(market_values)):
@@ -395,6 +525,7 @@ def execute_value_target_plan(
395525
if price is None:
396526
skipped.append({"symbol": symbol, "reason": "quote_unavailable"})
397527
continue
528+
reference_prices[symbol] = price
398529
tradable_deltas.append((symbol, delta_value, price))
399530

400531
for symbol, delta_value, price in [item for item in tradable_deltas if item[1] < 0]:
@@ -437,16 +568,17 @@ def execute_value_target_plan(
437568
buy_budget = min(float(delta_value), investable_cash)
438569
if order_notional_cap is not None:
439570
buy_budget = min(buy_budget, order_notional_cap)
440-
quantity = _floor_quantity(buy_budget / price)
571+
limit_price = _limit_buy_price(symbol, price, limit_buy_premium, limit_buy_premium_by_symbol)
572+
quantity = _floor_quantity(buy_budget / limit_price) if limit_price > 0 else 0
441573
if quantity <= 0:
442-
if order_notional_cap is None and investable_cash < price:
574+
if order_notional_cap is None and investable_cash < limit_price:
443575
skipped.append(
444576
{
445577
"symbol": symbol,
446578
"reason": "insufficient_cash_for_whole_share",
447-
"price": round(price, 2),
579+
"price": round(limit_price, 2),
448580
"investable_cash": round(investable_cash, 2),
449-
"required_cash_for_one_share": round(price, 2),
581+
"required_cash_for_one_share": round(limit_price, 2),
450582
}
451583
)
452584
else:
@@ -468,11 +600,23 @@ def execute_value_target_plan(
468600
symbol=symbol,
469601
side="buy",
470602
quantity=quantity,
471-
limit_price=price * float(limit_buy_premium),
603+
limit_price=limit_price,
472604
max_notional_usd=max_order_notional_usd,
473605
)
474606
)
475-
investable_cash = max(0.0, investable_cash - (quantity * price))
607+
investable_cash = max(0.0, investable_cash - (quantity * limit_price))
608+
609+
total_value = float(portfolio.get("total_equity") or portfolio.get("total_strategy_equity") or 0.0)
610+
drift_notes = build_small_account_allocation_drift_notes(
611+
target_values=small_account_reference_target_values,
612+
current_values=market_values,
613+
current_quantities=current_quantities,
614+
prices=reference_prices,
615+
submitted_orders=submitted,
616+
total_value=total_value,
617+
cash_value=float(portfolio.get("liquid_cash") or 0.0),
618+
)
619+
execution_notes = tuple(execution_notes) + tuple(drift_notes)
476620

477621
return ExecutionCycleResult(
478622
submitted_orders=tuple(submitted),

application/rebalance_service.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,40 @@
6666

6767
LIMIT_SELL_DISCOUNT = 0.995
6868
LIMIT_BUY_PREMIUM = 1.005
69+
DEFAULT_LIMIT_BUY_PREMIUM_BY_SYMBOL = {"SOXL": 1.015, "TQQQ": 1.010}
70+
71+
72+
def _load_limit_buy_premium_by_symbol(*env_names: str) -> dict[str, float]:
73+
raw_value = ""
74+
for env_name in env_names:
75+
value = os.getenv(env_name)
76+
if value and value.strip():
77+
raw_value = value.strip()
78+
break
79+
if not raw_value:
80+
return dict(DEFAULT_LIMIT_BUY_PREMIUM_BY_SYMBOL)
81+
try:
82+
payload = json.loads(raw_value)
83+
except json.JSONDecodeError as exc:
84+
raise ValueError(f"Invalid limit buy premium map JSON: {raw_value!r}") from exc
85+
if not isinstance(payload, dict):
86+
raise ValueError("Limit buy premium map must be a JSON object keyed by symbol.")
87+
parsed: dict[str, float] = {}
88+
for symbol, premium in payload.items():
89+
symbol_text = str(symbol or "").strip().upper()
90+
if not symbol_text:
91+
continue
92+
premium_value = float(premium)
93+
if premium_value <= 0.0:
94+
raise ValueError(f"Limit buy premium for {symbol_text} must be positive.")
95+
parsed[symbol_text] = premium_value
96+
return parsed
97+
98+
99+
LIMIT_BUY_PREMIUM_BY_SYMBOL = _load_limit_buy_premium_by_symbol(
100+
"FIRSTRADE_LIMIT_BUY_PREMIUM_BY_SYMBOL_JSON",
101+
"LIMIT_BUY_PREMIUM_BY_SYMBOL_JSON",
102+
)
69103

70104

71105
def _utcnow() -> datetime:
@@ -512,6 +546,7 @@ def log_message(message: str) -> None:
512546
dry_run_only=settings.dry_run_only,
513547
limit_sell_discount=LIMIT_SELL_DISCOUNT,
514548
limit_buy_premium=LIMIT_BUY_PREMIUM,
549+
limit_buy_premium_by_symbol=LIMIT_BUY_PREMIUM_BY_SYMBOL,
515550
max_order_notional_usd=settings.max_order_notional_usd,
516551
safe_haven_cash_substitute_threshold_usd=settings.safe_haven_cash_substitute_threshold_usd,
517552
)

0 commit comments

Comments
 (0)