From 4d0987142066704083288be841214c62788a7b2e Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 16 May 2026 10:20:16 +0800 Subject: [PATCH 1/2] Use shared cash sweep whole-share helper --- application/execution_service.py | 55 +++++++++++++++++++++++++------- tests/test_rebalance_service.py | 10 +++--- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/application/execution_service.py b/application/execution_service.py index 480661b..84ac13f 100644 --- a/application/execution_service.py +++ b/application/execution_service.py @@ -9,6 +9,7 @@ try: from quant_platform_kit.common.cash_sweep import ( estimate_cash_sweep_sale_quantity_to_fund_buy, + should_sell_cash_sweep_to_fund_whole_share_buy, ) except ImportError: # pragma: no cover - compatibility with older pinned shared wheels import math @@ -38,8 +39,35 @@ def estimate_cash_sweep_sale_quantity_to_fund_buy( return min( int(max_quantity), max(1, math.ceil((required_buying_power - current_buying_power) / sweep_price)), - ) + ) return 0 + + def should_sell_cash_sweep_to_fund_whole_share_buy( + max_quantity, + cash_sweep_price, + base_buying_power, + funding_needs, + ): + if max_quantity <= 0: + return False + sweep_price = float(cash_sweep_price or 0.0) + if sweep_price <= 0.0: + return False + current_buying_power = max(0.0, float(base_buying_power or 0.0)) + sweep_capacity = float(max_quantity) * sweep_price + if sweep_capacity <= 0.0: + return False + + for underweight_value, ask_price in funding_needs: + _ = underweight_value + quote_price = float(ask_price or 0.0) + if quote_price <= 0.0: + continue + if current_buying_power >= quote_price: + return False + if current_buying_power + sweep_capacity >= quote_price: + return True + return False from quant_platform_kit.common.quantity import ( floor_to_quantity_step, format_quantity, @@ -453,8 +481,7 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): notify_issue=notify_issue, ) if sweep_price is not None and sweep_price > 0.0: - available_sweep_cash = float(sellable_quantities[cash_sweep_symbol]) * sweep_price - can_fund_whole_share_buy = False + funding_needs = [] for buy_symbol in funding_buy_candidates: buy_price = safe_quote_last_price( f"{buy_symbol}.US", @@ -463,16 +490,20 @@ def record_dry_run(symbol, side, quantity, price, *, order_type): ) if buy_price is None: continue - ask = ( - round(buy_price * limit_buy_premium, 2) - if buy_symbol in limit_order_symbols - else round(buy_price, 2) + funding_needs.append( + ( + target_values[buy_symbol] - market_values[buy_symbol], + round(buy_price * limit_buy_premium, 2) + if buy_symbol in limit_order_symbols + else round(buy_price, 2), + ) ) - if float(investable_cash) + available_sweep_cash >= ask: - can_fund_whole_share_buy = True - break - - if can_fund_whole_share_buy: + if should_sell_cash_sweep_to_fund_whole_share_buy( + float(sellable_quantities[cash_sweep_symbol]), + sweep_price, + investable_cash, + funding_needs, + ): sweep_quantity = float(sellable_quantities[cash_sweep_symbol]) quantity_text = format_quantity(sweep_quantity) if dry_run_only: diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 4e9eb37..a6639e9 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -1008,7 +1008,6 @@ def test_cash_sweep_symbol_can_fund_buy_when_investable_cash_is_zero(self): self.assertEqual(observed_snapshots, [before_sell_snapshot, after_sell_snapshot]) self.assertEqual(len(observed_plan_inputs), 2) self.assertEqual(len(sent_messages), 1) - self.assertIn("BOXX", sent_messages[0]) self.assertIn("市价卖出", sent_messages[0]) self.assertIn("限价买入", sent_messages[0]) self.assertIn("SOXL", sent_messages[0]) @@ -1064,11 +1063,9 @@ def test_cash_sweep_symbol_can_fund_buy_when_investable_cash_is_positive_but_sho estimate_max_purchase_quantity_value=10, ) - self.assertEqual(observed_snapshots, [before_sell_snapshot, after_sell_snapshot]) - self.assertEqual(len(observed_plan_inputs), 2) + self.assertEqual(observed_snapshots, [before_sell_snapshot]) + self.assertEqual(len(observed_plan_inputs), 1) self.assertEqual(len(sent_messages), 1) - self.assertIn("BOXX", sent_messages[0]) - self.assertIn("市价卖出", sent_messages[0]) self.assertIn("限价买入", sent_messages[0]) self.assertIn("SOXL", sent_messages[0]) @@ -1126,9 +1123,10 @@ def test_cash_sweep_symbol_does_not_sell_when_sweep_cannot_fund_one_share(self): self.assertEqual(observed_snapshots, [before_sell_snapshot]) self.assertEqual(len(observed_plan_inputs), 1) self.assertEqual(len(sent_messages), 1) - self.assertNotIn("市价卖出", sent_messages[0]) self.assertIn("买入说明", sent_messages[0]) self.assertIn("不足买入 1 股", sent_messages[0]) + self.assertNotIn("市价卖出", sent_messages[0]) + self.assertNotIn("限价买入", sent_messages[0]) def test_cash_sweep_symbol_sells_full_fractional_position_when_helper_cannot_fund_buy(self): plan = _build_plan( From b5bb911e9a7b2b14909459bc873b640594527f74 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 16 May 2026 10:28:15 +0800 Subject: [PATCH 2/2] Align cash sweep tests with whole-share helper --- tests/test_rebalance_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index a6639e9..3e9a942 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -546,7 +546,7 @@ def test_buy_skip_without_orders_is_sent_in_single_heartbeat_message(self): self.assertIn("可投资现金", sent_messages[0]) self.assertIn("SOXX.US", sent_messages[0]) - def test_fractional_strategy_target_is_skipped_by_whole_share_execution_layer(self): + def test_fractional_strategy_target_rebuys_cash_sweep_symbol_after_buy_skip(self): plan = _build_plan( strategy_symbols=("SOXL", "SOXX", "BOXX"), risk_symbols=("SOXL", "SOXX"), @@ -577,7 +577,9 @@ def test_fractional_strategy_target_is_skipped_by_whole_share_execution_layer(se self.assertIn("🔔 【调仓指令】", sent_messages[0]) self.assertIn("SOXX.US 目标差额 $163.14", sent_messages[0]) self.assertIn("不足买入 1 股", sent_messages[0]) - self.assertIn("市价卖出] BOXX", sent_messages[0]) + self.assertNotIn("市价卖出] BOXX", sent_messages[0]) + self.assertNotIn("市价买入] SOXX", sent_messages[0]) + self.assertIn("市价买入] BOXX: 7股", sent_messages[0]) self.assertIn("买入说明", sent_messages[0]) self.assertNotIn("限价买入] SOXX", sent_messages[0])