11import asyncio
2+ from types import SimpleNamespace
23
34import pytest
4- from ib_async import IB , Contract , Order , OrderStatus , Stock , Ticker , Trade
5+ from ib_async import (
6+ IB ,
7+ AccountValue ,
8+ Contract ,
9+ Order ,
10+ OrderStatus ,
11+ Stock ,
12+ Ticker ,
13+ Trade ,
14+ )
515
616from thetagang import log
717from thetagang .ibkr import (
@@ -25,6 +35,8 @@ def mock_ib(mocker):
2535 mock .orderStatusEvent .__iadd__ = mocker .Mock (
2636 return_value = None
2737 ) # Allow += operation
38+ mock .wrapper = mocker .Mock ()
39+ mock .wrapper .accountValues = {}
2840 return mock
2941
3042
@@ -268,6 +280,7 @@ async def test_refresh_account_updates_uses_timeout_wrapper(ibkr, mocker):
268280 req_future : asyncio .Future = asyncio .get_running_loop ().create_future ()
269281 req_future .set_result (None )
270282 ibkr .ib .reqAccountUpdatesAsync = mocker .Mock (return_value = req_future )
283+ mocker .patch .object (ibkr , "_account_snapshot_ready" , side_effect = [False , True ])
271284 await_wrapper = mocker .patch .object (
272285 ibkr , "_await_with_timeout" , new = mocker .AsyncMock (return_value = None )
273286 )
@@ -317,6 +330,106 @@ async def test_refresh_account_updates_propagates_timeout(ibkr, mocker):
317330 await ibkr .refresh_account_updates ("ACC123" )
318331
319332
333+ async def test_refresh_account_updates_skips_when_snapshot_ready (ibkr , mocker ):
334+ """No request issued when account snapshot already populated."""
335+ mocker .patch .object (ibkr , "_account_snapshot_ready" , return_value = True )
336+
337+ await ibkr .refresh_account_updates ("ACC123" )
338+
339+ ibkr .ib .reqAccountUpdatesAsync .assert_not_called ()
340+
341+
342+ async def test_refresh_account_updates_allows_timeout_if_data_ready (ibkr , mocker ):
343+ """A timeout is ignored when snapshot becomes ready while waiting."""
344+ mocker .patch .object (ibkr , "_account_snapshot_ready" , side_effect = [False , True ])
345+ ibkr .ib .reqAccountUpdatesAsync = mocker .Mock (return_value = object ())
346+ mocker .patch .object (
347+ ibkr ,
348+ "_await_with_timeout" ,
349+ new = mocker .AsyncMock (
350+ side_effect = IBKRRequestTimeout (
351+ "account updates" , ibkr .api_response_wait_time
352+ )
353+ ),
354+ )
355+
356+ await ibkr .refresh_account_updates ("ACC123" )
357+
358+ assert ibkr ._account_snapshot_ready .call_count == 2
359+
360+
361+ async def test_refresh_account_updates_raises_when_snapshot_never_populates (
362+ ibkr , mocker
363+ ):
364+ """If data never arrives, an IBKRRequestTimeout is raised."""
365+ mocker .patch .object (ibkr , "_account_snapshot_ready" , return_value = False )
366+ ibkr .ib .reqAccountUpdatesAsync = mocker .Mock (return_value = object ())
367+ mocker .patch .object (
368+ ibkr , "_await_with_timeout" , new = mocker .AsyncMock (return_value = None )
369+ )
370+
371+ with pytest .raises (IBKRRequestTimeout ) as excinfo :
372+ await ibkr .refresh_account_updates ("ACC123" )
373+
374+ assert "no usable account values" in str (excinfo .value )
375+
376+
377+ async def test_account_snapshot_ready_checks_for_non_zero_account_values (ibkr , mock_ib ):
378+ """Helper returns True only when tracked tags have non-zero data."""
379+ mock_ib .wrapper .accountValues = {
380+ ("ACC123" , "NetLiquidation" , "USD" , "" ): AccountValue (
381+ "ACC123" , "NetLiquidation" , "0" , "USD" , ""
382+ )
383+ }
384+
385+ assert ibkr ._account_snapshot_ready ("ACC123" ) is False
386+
387+ mock_ib .wrapper .accountValues = {
388+ ("ACC123" , "NetLiquidation" , "USD" , "" ): AccountValue (
389+ "ACC123" , "NetLiquidation" , "100000" , "USD" , ""
390+ )
391+ }
392+
393+ assert ibkr ._account_snapshot_ready ("ACC123" ) is True
394+
395+
396+ async def test_account_snapshot_ready_ignores_other_accounts_and_tags (ibkr , mock_ib ):
397+ """Values for other accounts or untracked tags should not mark snapshot ready."""
398+ mock_ib .wrapper .accountValues = {
399+ ("OTHER" , "NetLiquidation" , "USD" , "" ): AccountValue (
400+ "OTHER" , "NetLiquidation" , "100000" , "USD" , ""
401+ ),
402+ ("ACC123" , "GrossPositionValue" , "USD" , "" ): AccountValue (
403+ "ACC123" , "GrossPositionValue" , "5000" , "USD" , ""
404+ ),
405+ }
406+
407+ assert ibkr ._account_snapshot_ready ("ACC123" ) is False
408+
409+
410+ async def test_account_snapshot_ready_handles_missing_wrapper_or_values (ibkr , mock_ib ):
411+ """Return False when wrapper or accountValues are absent."""
412+ mock_ib .wrapper .accountValues = {}
413+ assert ibkr ._account_snapshot_ready ("ACC123" ) is False
414+
415+ mock_ib .wrapper = None
416+ assert ibkr ._account_snapshot_ready ("ACC123" ) is False
417+
418+
419+ async def test_account_value_has_data_true_for_non_zero_numeric (ibkr ):
420+ """Helper treats any non-zero numeric string as usable data."""
421+ value = AccountValue ("ACC123" , "NetLiquidation" , "123.45" , "USD" , "" )
422+ assert ibkr ._account_value_has_data (value ) is True
423+
424+
425+ @pytest .mark .parametrize ("raw_value" , ["0" , "0.0" , "" , None , "abc" ])
426+ async def test_account_value_has_data_false_for_invalid_inputs (ibkr , raw_value ):
427+ """Helper rejects zero, empty, None, and non-numeric values."""
428+ value = SimpleNamespace (value = raw_value )
429+
430+ assert ibkr ._account_value_has_data (value ) is False
431+
432+
320433async def test_await_with_timeout_wraps_timeout_error (ibkr , mocker ):
321434 """_await_with_timeout raises IBKRRequestTimeout on asyncio timeout."""
322435
0 commit comments