Skip to content

Commit 573ee5d

Browse files
committed
fix: refresh account state before follow-up buys
1 parent 979069e commit 573ee5d

2 files changed

Lines changed: 138 additions & 10 deletions

File tree

application/rebalance_service.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,45 @@ def record_skip_log(skip_logs, *, translator, with_prefix, kind, detail):
1515
print(with_prefix(message), flush=True)
1616

1717

18+
def resolve_rebalance_plan(
19+
*,
20+
indicators,
21+
account_state,
22+
trend_ma_window,
23+
translator,
24+
cash_reserve_ratio,
25+
min_trade_ratio,
26+
min_trade_floor,
27+
rebalance_threshold_ratio,
28+
small_account_deploy_ratio,
29+
mid_account_deploy_ratio,
30+
large_account_deploy_ratio,
31+
trade_layer_decay_coeff,
32+
income_layer_start_usd,
33+
income_layer_max_ratio,
34+
income_layer_qqqi_weight,
35+
income_layer_spyi_weight,
36+
):
37+
return build_rebalance_plan(
38+
indicators,
39+
account_state,
40+
trend_ma_window=trend_ma_window,
41+
translator=translator,
42+
cash_reserve_ratio=cash_reserve_ratio,
43+
min_trade_ratio=min_trade_ratio,
44+
min_trade_floor=min_trade_floor,
45+
rebalance_threshold_ratio=rebalance_threshold_ratio,
46+
small_account_deploy_ratio=small_account_deploy_ratio,
47+
mid_account_deploy_ratio=mid_account_deploy_ratio,
48+
large_account_deploy_ratio=large_account_deploy_ratio,
49+
trade_layer_decay_coeff=trade_layer_decay_coeff,
50+
income_layer_start_usd=income_layer_start_usd,
51+
income_layer_max_ratio=income_layer_max_ratio,
52+
income_layer_qqqi_weight=income_layer_qqqi_weight,
53+
income_layer_spyi_weight=income_layer_spyi_weight,
54+
)
55+
56+
1857
def run_strategy(
1958
*,
2059
project_id,
@@ -69,9 +108,9 @@ def run_strategy(
69108

70109
strategy_assets = ["SOXL", "SOXX", "BOXX", "QQQI", "SPYI"]
71110
account_state = fetch_strategy_account_state(quote_context, trade_context, strategy_assets)
72-
plan = build_rebalance_plan(
73-
indicators,
74-
account_state,
111+
plan = resolve_rebalance_plan(
112+
indicators=indicators,
113+
account_state=account_state,
75114
trend_ma_window=trend_ma_window,
76115
translator=translator,
77116
cash_reserve_ratio=cash_reserve_ratio,
@@ -91,6 +130,7 @@ def run_strategy(
91130
logs = []
92131
skip_logs = []
93132
action_done = False
133+
sell_submitted = False
94134
threshold_value = plan["threshold_value"]
95135
limit_order_symbols = set(plan["limit_order_symbols"])
96136

@@ -135,6 +175,7 @@ def run_strategy(
135175

136176
if submitted:
137177
action_done = True
178+
sell_submitted = True
138179
elif plan["sellable_quantities"][symbol] <= 0 and plan["quantities"][symbol] > 0:
139180
record_skip_log(
140181
skip_logs,
@@ -148,6 +189,29 @@ def run_strategy(
148189
),
149190
)
150191

192+
if sell_submitted:
193+
account_state = fetch_strategy_account_state(quote_context, trade_context, strategy_assets)
194+
plan = resolve_rebalance_plan(
195+
indicators=indicators,
196+
account_state=account_state,
197+
trend_ma_window=trend_ma_window,
198+
translator=translator,
199+
cash_reserve_ratio=cash_reserve_ratio,
200+
min_trade_ratio=min_trade_ratio,
201+
min_trade_floor=min_trade_floor,
202+
rebalance_threshold_ratio=rebalance_threshold_ratio,
203+
small_account_deploy_ratio=small_account_deploy_ratio,
204+
mid_account_deploy_ratio=mid_account_deploy_ratio,
205+
large_account_deploy_ratio=large_account_deploy_ratio,
206+
trade_layer_decay_coeff=trade_layer_decay_coeff,
207+
income_layer_start_usd=income_layer_start_usd,
208+
income_layer_max_ratio=income_layer_max_ratio,
209+
income_layer_qqqi_weight=income_layer_qqqi_weight,
210+
income_layer_spyi_weight=income_layer_spyi_weight,
211+
)
212+
threshold_value = plan["threshold_value"]
213+
limit_order_symbols = set(plan["limit_order_symbols"])
214+
151215
investable_cash = plan["investable_cash"]
152216
for symbol in plan["strategy_assets"]:
153217
diff = plan["targets"][symbol] - plan["market_values"][symbol]

tests/test_rebalance_service.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
1919

2020

2121
class RebalanceServiceNotificationTests(unittest.TestCase):
22-
def _run_strategy(self, plan, *, prices):
22+
def _run_strategy(
23+
self,
24+
plan,
25+
*,
26+
prices,
27+
refreshed_plan=None,
28+
account_states=None,
29+
estimate_max_purchase_quantity_value=0,
30+
):
2331
sent_messages = []
32+
observed_account_states = []
2433

2534
def fake_send_tg_message(message):
2635
sent_messages.append(message)
@@ -43,7 +52,19 @@ def fake_submit_order_with_alert(
4352
logs.append(f"{log_message} [order_id=test-order]")
4453
return True
4554

46-
with patch.object(rebalance_service, "build_rebalance_plan", return_value=plan):
55+
plan_side_effect = [plan, refreshed_plan or plan]
56+
57+
account_state_values = list(account_states or [{}, {}])
58+
59+
def fake_fetch_strategy_account_state(quote_context, trade_context, strategy_assets):
60+
del quote_context, trade_context, strategy_assets
61+
if not account_state_values:
62+
raise AssertionError("unexpected extra fetch_strategy_account_state call")
63+
value = account_state_values.pop(0)
64+
observed_account_states.append(value)
65+
return value
66+
67+
with patch.object(rebalance_service, "build_rebalance_plan", side_effect=plan_side_effect):
4768
rebalance_service.run_strategy(
4869
project_id="project-1",
4970
secret_name="secret-1",
@@ -72,13 +93,13 @@ def fake_submit_order_with_alert(
7293
refresh_token_if_needed=lambda *args, **kwargs: "live-token",
7394
build_contexts=lambda app_key, app_secret, token: ("quote-context", "trade-context"),
7495
calculate_rotation_indicators=lambda quote_context, trend_window: {"soxl": {"price": 1, "ma_trend": 2}},
75-
fetch_strategy_account_state=lambda quote_context, trade_context, strategy_assets: {},
96+
fetch_strategy_account_state=fake_fetch_strategy_account_state,
7697
fetch_last_price=lambda quote_context, symbol: prices[symbol],
77-
estimate_max_purchase_quantity=lambda *args, **kwargs: 0,
98+
estimate_max_purchase_quantity=lambda *args, **kwargs: estimate_max_purchase_quantity_value,
7899
submit_order_with_alert=fake_submit_order_with_alert,
79100
)
80101

81-
return sent_messages
102+
return sent_messages, observed_account_states
82103

83104
def test_sell_then_buy_skip_is_sent_in_single_summary_message(self):
84105
plan = {
@@ -100,7 +121,7 @@ def test_sell_then_buy_skip_is_sent_in_single_summary_message(self):
100121
"total_strategy_equity": 60000.0,
101122
"portfolio_rows": (("SOXL", "SOXX"),),
102123
}
103-
sent_messages = self._run_strategy(
124+
sent_messages, _ = self._run_strategy(
104125
plan,
105126
prices={"SOXL.US": 45.94, "SOXX.US": 322.74},
106127
)
@@ -131,7 +152,7 @@ def test_buy_skip_without_orders_is_sent_in_single_heartbeat_message(self):
131152
"total_strategy_equity": 60000.0,
132153
"portfolio_rows": (("SOXX",),),
133154
}
134-
sent_messages = self._run_strategy(
155+
sent_messages, _ = self._run_strategy(
135156
plan,
136157
prices={"SOXX.US": 322.74},
137158
)
@@ -142,6 +163,49 @@ def test_buy_skip_without_orders_is_sent_in_single_heartbeat_message(self):
142163
self.assertIn("跳过项", sent_messages[0])
143164
self.assertIn("SOXX.US", sent_messages[0])
144165

166+
def test_refreshes_account_state_after_sell_and_can_place_followup_buy(self):
167+
initial_plan = {
168+
"strategy_assets": ["SOXL", "SOXX"],
169+
"targets": {"SOXL": 0.0, "SOXX": 34718.05},
170+
"market_values": {"SOXL": 31928.30, "SOXX": 0.0},
171+
"sellable_quantities": {"SOXL": 695, "SOXX": 0},
172+
"quantities": {"SOXL": 695, "SOXX": 0},
173+
"current_min_trade": 100.0,
174+
"limit_order_symbols": ("SOXL", "SOXX"),
175+
"threshold_value": 100.0,
176+
"investable_cash": 101.95,
177+
"market_status": "🛡️ DE-LEVER (SOXX)",
178+
"deploy_ratio_text": "57.9%",
179+
"income_ratio_text": "0.0%",
180+
"income_locked_ratio_text": "38.3%",
181+
"signal_message": "SOXL 跌破 150 日均线,切换至 SOXX,交易层风险仓位 57.9%",
182+
"available_cash": 101.95,
183+
"total_strategy_equity": 60000.0,
184+
"portfolio_rows": (("SOXL", "SOXX"),),
185+
}
186+
refreshed_plan = {
187+
**initial_plan,
188+
"market_values": {"SOXL": 0.0, "SOXX": 0.0},
189+
"sellable_quantities": {"SOXL": 0, "SOXX": 0},
190+
"quantities": {"SOXL": 0, "SOXX": 0},
191+
"investable_cash": 40000.0,
192+
"available_cash": 40000.0,
193+
}
194+
sent_messages, observed_account_states = self._run_strategy(
195+
initial_plan,
196+
refreshed_plan=refreshed_plan,
197+
account_states=[{"phase": "before_sell"}, {"phase": "after_sell"}],
198+
prices={"SOXL.US": 45.94, "SOXX.US": 322.74},
199+
estimate_max_purchase_quantity_value=200,
200+
)
201+
202+
self.assertEqual(observed_account_states, [{"phase": "before_sell"}, {"phase": "after_sell"}])
203+
self.assertEqual(len(sent_messages), 1)
204+
self.assertIn("🔔 【调仓指令】", sent_messages[0])
205+
self.assertIn("限价卖出", sent_messages[0])
206+
self.assertIn("限价买入", sent_messages[0])
207+
self.assertNotIn("买入跳过", sent_messages[0])
208+
145209

146210
if __name__ == "__main__":
147211
unittest.main()

0 commit comments

Comments
 (0)