1919
2020
2121class 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
146210if __name__ == "__main__" :
147211 unittest .main ()
0 commit comments