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
51 changes: 48 additions & 3 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ def estimate_cash_sweep_sale_quantity_to_fund_buy(
quote_price = float(ask_price or 0.0)
if needed_value <= 0.0 or quote_price <= 0.0:
continue
max_buy_quantity = int(needed_value // quote_price)
if max_buy_quantity <= 0:
continue
max_buy_quantity = max(1, int(needed_value // quote_price))
required_buying_power = max_buy_quantity * quote_price
if current_buying_power >= required_buying_power:
return 0
Expand Down Expand Up @@ -529,6 +527,53 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity, candidate_symbols):
sell_submitted = True
cash_sweep_sold_this_cycle = True

if (
not sell_submitted
and funding_buy_candidates
and cash_sweep_symbol
and sellable_quantities.get(cash_sweep_symbol, 0.0) > 0.0
):
sweep_quantity = cash_sweep_sale_quantity_to_fund_buy(
int(sellable_quantities[cash_sweep_symbol]),
funding_buy_candidates,
)
if sweep_quantity <= 0:
sweep_quantity = 1
Comment on lines +540 to +541
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 Do not force BOXX sale when funding need is zero

When cash_sweep_sale_quantity_to_fund_buy(...) returns 0 (for example, because current investable cash already covers the next buy), this branch overwrites it to 1 and still submits a BOXX sell. In execute_rebalance_cycle, that means any cycle with a buy candidate and sellable BOXX can generate an unnecessary market sell even though no additional funding is needed, causing avoidable churn and allocation drift.

Useful? React with 👍 / 👎.

sweep_price = safe_quote_last_price(
f"{cash_sweep_symbol}.US",
market_data_port=market_data_port,
notify_issue=notify_issue,
)
if sweep_price is not None and sweep_price > 0.0:
quantity_text = format_quantity(sweep_quantity)
if dry_run_only:
submitted = record_dry_run(
f"{cash_sweep_symbol}.US",
"sell",
quantity_text,
round(sweep_price, 2),
order_type="market",
)
if submitted:
dry_run_sale_proceeds += float(sweep_quantity) * round(sweep_price, 2)
else:
submitted = submit_order_via_port(
f"{cash_sweep_symbol}.US",
"market",
"sell",
sweep_quantity,
translator(
"market_sell",
symbol=cash_sweep_symbol,
qty=quantity_text,
price=round(sweep_price, 2),
),
)
if submitted:
action_done = True
sell_submitted = True
cash_sweep_sold_this_cycle = True

if sell_submitted:
if dry_run_only and dry_run_sale_proceeds > 0.0:
simulated_cash = float(dry_run_sale_proceeds)
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8769362096227320bc05c791b5244d4b3e88db50
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ed55a6af0245323dbed82060e89be96d8f77f756
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@3c8262d1df7d11e47e5ffbff83784544d10f4b9b
pandas
requests
pytz
Expand Down
61 changes: 60 additions & 1 deletion tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,8 @@ 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("尾部回补", sent_messages[0])
self.assertIn("市价卖出] BOXX", sent_messages[0])
self.assertIn("买入说明", sent_messages[0])
self.assertNotIn("限价买入] SOXX", sent_messages[0])

def test_fractional_strategy_target_buy_floors_to_cash_backed_whole_shares(self):
Expand Down Expand Up @@ -1071,6 +1072,64 @@ def test_cash_sweep_symbol_can_fund_buy_when_investable_cash_is_positive_but_sho
self.assertIn("限价买入", sent_messages[0])
self.assertIn("SOXL", sent_messages[0])

def test_cash_sweep_symbol_sells_even_when_underweight_is_below_one_share(self):
initial_plan = _build_plan(
strategy_symbols=("SOXL", "SOXX", "BOXX"),
risk_symbols=("SOXL", "SOXX"),
safe_haven_symbols=("BOXX",),
targets={"SOXL": 167.79, "SOXX": 0.0, "BOXX": 1000.0},
market_values={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 1000.0},
sellable_quantities={"SOXL": 0, "SOXX": 0, "BOXX": 1},
quantities={"SOXL": 0, "SOXX": 0, "BOXX": 1},
current_min_trade=100.0,
trade_threshold_value=100.0,
investable_cash=14.46,
market_status="🧯 过热降档(SOXX)",
deploy_ratio_text="15.0%",
income_ratio_text="0.0%",
income_locked_ratio_text="0.0%",
signal_message="SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%",
available_cash=14.46,
total_strategy_equity=1000.0,
portfolio_rows=(("SOXL", "SOXX"), ("BOXX",)),
)
refreshed_plan = _build_plan(
strategy_symbols=("SOXL", "SOXX", "BOXX"),
risk_symbols=("SOXL", "SOXX"),
safe_haven_symbols=("BOXX",),
targets={"SOXL": 167.79, "SOXX": 0.0, "BOXX": 1000.0},
market_values={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 900.0},
sellable_quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0},
quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0},
current_min_trade=100.0,
trade_threshold_value=100.0,
investable_cash=114.46,
market_status="🧯 过热降档(SOXX)",
deploy_ratio_text="15.0%",
income_ratio_text="0.0%",
income_locked_ratio_text="0.0%",
signal_message="SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%",
available_cash=114.46,
total_strategy_equity=1000.0,
portfolio_rows=(("SOXL", "SOXX"), ("BOXX",)),
)
before_sell_snapshot = _build_snapshot(initial_plan, phase="before_cash_sweep_small_gap")
after_sell_snapshot = _build_snapshot(refreshed_plan, phase="after_cash_sweep_small_gap")
sent_messages, observed_snapshots, observed_plan_inputs = self._run_strategy(
initial_plan,
refreshed_plan=refreshed_plan,
portfolio_snapshots=[before_sell_snapshot, after_sell_snapshot],
prices={"SOXL.US": 167.79, "SOXX.US": 200.0, "BOXX.US": 100.0},
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(len(sent_messages), 1)
self.assertIn("BOXX", sent_messages[0])
self.assertIn("市价卖出", sent_messages[0])
self.assertNotIn("买入跳过", sent_messages[0])

def test_dry_run_cash_sweep_can_simulate_buy_after_sell_settlement(self):
initial_plan = _build_plan(
strategy_symbols=("SOXL", "SOXX", "BOXX"),
Expand Down