Skip to content

Commit 59f8630

Browse files
committed
fix: sell cash sweep on longbridge when buying power is short
1 parent 15edc6f commit 59f8630

3 files changed

Lines changed: 108 additions & 5 deletions

File tree

application/execution_service.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ def estimate_cash_sweep_sale_quantity_to_fund_buy(
3131
quote_price = float(ask_price or 0.0)
3232
if needed_value <= 0.0 or quote_price <= 0.0:
3333
continue
34-
max_buy_quantity = int(needed_value // quote_price)
35-
if max_buy_quantity <= 0:
36-
continue
34+
max_buy_quantity = max(1, int(needed_value // quote_price))
3735
required_buying_power = max_buy_quantity * quote_price
3836
if current_buying_power >= required_buying_power:
3937
return 0
@@ -529,6 +527,53 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity, candidate_symbols):
529527
sell_submitted = True
530528
cash_sweep_sold_this_cycle = True
531529

530+
if (
531+
not sell_submitted
532+
and funding_buy_candidates
533+
and cash_sweep_symbol
534+
and sellable_quantities.get(cash_sweep_symbol, 0.0) > 0.0
535+
):
536+
sweep_quantity = cash_sweep_sale_quantity_to_fund_buy(
537+
int(sellable_quantities[cash_sweep_symbol]),
538+
funding_buy_candidates,
539+
)
540+
if sweep_quantity <= 0:
541+
sweep_quantity = 1
542+
sweep_price = safe_quote_last_price(
543+
f"{cash_sweep_symbol}.US",
544+
market_data_port=market_data_port,
545+
notify_issue=notify_issue,
546+
)
547+
if sweep_price is not None and sweep_price > 0.0:
548+
quantity_text = format_quantity(sweep_quantity)
549+
if dry_run_only:
550+
submitted = record_dry_run(
551+
f"{cash_sweep_symbol}.US",
552+
"sell",
553+
quantity_text,
554+
round(sweep_price, 2),
555+
order_type="market",
556+
)
557+
if submitted:
558+
dry_run_sale_proceeds += float(sweep_quantity) * round(sweep_price, 2)
559+
else:
560+
submitted = submit_order_via_port(
561+
f"{cash_sweep_symbol}.US",
562+
"market",
563+
"sell",
564+
sweep_quantity,
565+
translator(
566+
"market_sell",
567+
symbol=cash_sweep_symbol,
568+
qty=quantity_text,
569+
price=round(sweep_price, 2),
570+
),
571+
)
572+
if submitted:
573+
action_done = True
574+
sell_submitted = True
575+
cash_sweep_sold_this_cycle = True
576+
532577
if sell_submitted:
533578
if dry_run_only and dry_run_sale_proceeds > 0.0:
534579
simulated_cash = float(dry_run_sale_proceeds)

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
3-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8769362096227320bc05c791b5244d4b3e88db50
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ed55a6af0245323dbed82060e89be96d8f77f756
3+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@663e80be60b0da80e81513b711c579d221a2111d
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b189190cd32a64206178c40038aa51d65c904648
55
pandas
66
requests
77
pytz

tests/test_rebalance_service.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,64 @@ def test_cash_sweep_symbol_can_fund_buy_when_investable_cash_is_positive_but_sho
10711071
self.assertIn("限价买入", sent_messages[0])
10721072
self.assertIn("SOXL", sent_messages[0])
10731073

1074+
def test_cash_sweep_symbol_sells_even_when_underweight_is_below_one_share(self):
1075+
initial_plan = _build_plan(
1076+
strategy_symbols=("SOXL", "SOXX", "BOXX"),
1077+
risk_symbols=("SOXL", "SOXX"),
1078+
safe_haven_symbols=("BOXX",),
1079+
targets={"SOXL": 167.79, "SOXX": 0.0, "BOXX": 1000.0},
1080+
market_values={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 1000.0},
1081+
sellable_quantities={"SOXL": 0, "SOXX": 0, "BOXX": 1},
1082+
quantities={"SOXL": 0, "SOXX": 0, "BOXX": 1},
1083+
current_min_trade=100.0,
1084+
trade_threshold_value=100.0,
1085+
investable_cash=14.46,
1086+
market_status="🧯 过热降档(SOXX)",
1087+
deploy_ratio_text="15.0%",
1088+
income_ratio_text="0.0%",
1089+
income_locked_ratio_text="0.0%",
1090+
signal_message="SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%",
1091+
available_cash=14.46,
1092+
total_strategy_equity=1000.0,
1093+
portfolio_rows=(("SOXL", "SOXX"), ("BOXX",)),
1094+
)
1095+
refreshed_plan = _build_plan(
1096+
strategy_symbols=("SOXL", "SOXX", "BOXX"),
1097+
risk_symbols=("SOXL", "SOXX"),
1098+
safe_haven_symbols=("BOXX",),
1099+
targets={"SOXL": 167.79, "SOXX": 0.0, "BOXX": 1000.0},
1100+
market_values={"SOXL": 0.0, "SOXX": 0.0, "BOXX": 900.0},
1101+
sellable_quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0},
1102+
quantities={"SOXL": 0, "SOXX": 0, "BOXX": 0},
1103+
current_min_trade=100.0,
1104+
trade_threshold_value=100.0,
1105+
investable_cash=114.46,
1106+
market_status="🧯 过热降档(SOXX)",
1107+
deploy_ratio_text="15.0%",
1108+
income_ratio_text="0.0%",
1109+
income_locked_ratio_text="0.0%",
1110+
signal_message="SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%",
1111+
available_cash=114.46,
1112+
total_strategy_equity=1000.0,
1113+
portfolio_rows=(("SOXL", "SOXX"), ("BOXX",)),
1114+
)
1115+
before_sell_snapshot = _build_snapshot(initial_plan, phase="before_cash_sweep_small_gap")
1116+
after_sell_snapshot = _build_snapshot(refreshed_plan, phase="after_cash_sweep_small_gap")
1117+
sent_messages, observed_snapshots, observed_plan_inputs = self._run_strategy(
1118+
initial_plan,
1119+
refreshed_plan=refreshed_plan,
1120+
portfolio_snapshots=[before_sell_snapshot, after_sell_snapshot],
1121+
prices={"SOXL.US": 167.79, "SOXX.US": 200.0, "BOXX.US": 100.0},
1122+
estimate_max_purchase_quantity_value=10,
1123+
)
1124+
1125+
self.assertEqual(observed_snapshots, [before_sell_snapshot, after_sell_snapshot])
1126+
self.assertEqual(len(observed_plan_inputs), 2)
1127+
self.assertEqual(len(sent_messages), 1)
1128+
self.assertIn("BOXX", sent_messages[0])
1129+
self.assertIn("市价卖出", sent_messages[0])
1130+
self.assertNotIn("买入跳过", sent_messages[0])
1131+
10741132
def test_dry_run_cash_sweep_can_simulate_buy_after_sell_settlement(self):
10751133
initial_plan = _build_plan(
10761134
strategy_symbols=("SOXL", "SOXX", "BOXX"),

0 commit comments

Comments
 (0)