|
19 | 19 | import octobot_node.scheduler.tasks as scheduler_tasks |
20 | 20 | import octobot_node.scheduler.user_actions.user_actions_executor.util.account_state_updater as account_state_updater_module |
21 | 21 | import octobot_protocol.models as protocol_models |
| 22 | +import octobot_trading.enums as trading_enums |
22 | 23 | import octobot_trading.errors as trading_errors |
23 | 24 | import octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector_module |
24 | 25 |
|
|
30 | 31 | _WORKFLOW_RESULT_TIMEOUT_SECONDS = 120.0 |
31 | 32 | _USER_ACTION_LIST_POLL_TIMEOUT_SECONDS = 15.0 |
32 | 33 |
|
| 34 | +_FUNCTIONAL_USDT_HOLDINGS = 1000.0 |
| 35 | +_FUNCTIONAL_BTC_HOLDINGS = 0.5 |
| 36 | +_FUNCTIONAL_ETH_HOLDINGS = 2.0 |
| 37 | +_FUNCTIONAL_BTC_USDT_CLOSE = 50000.0 |
| 38 | +_EXPECTED_PORTFOLIO_UNIT = "USDT" |
| 39 | +_EXPECTED_USDT_VALUE = _FUNCTIONAL_USDT_HOLDINGS |
| 40 | +_EXPECTED_BTC_VALUE = _FUNCTIONAL_BTC_HOLDINGS * _FUNCTIONAL_BTC_USDT_CLOSE |
| 41 | +_EXPECTED_ETH_VALUE = 0.0 |
| 42 | +_EXPECTED_PORTFOLIO_TOTAL = _EXPECTED_USDT_VALUE + _EXPECTED_BTC_VALUE + _EXPECTED_ETH_VALUE |
| 43 | + |
33 | 44 | _REAL_UPDATE_ACCOUNT_STATE = account_state_updater_module.update_account_state |
34 | 45 |
|
35 | 46 |
|
@@ -68,14 +79,29 @@ async def _stub_load_symbol_markets_no_network(self, reload=False, market_filter |
68 | 79 | async def _stub_get_balance_no_network(self, **kwargs): |
69 | 80 | return { |
70 | 81 | "USDT": { |
71 | | - commons_constants.PORTFOLIO_TOTAL: 1000.0, |
72 | | - commons_constants.PORTFOLIO_AVAILABLE: 1000.0, |
73 | | - } |
| 82 | + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_USDT_HOLDINGS, |
| 83 | + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_USDT_HOLDINGS, |
| 84 | + }, |
| 85 | + "BTC": { |
| 86 | + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_BTC_HOLDINGS, |
| 87 | + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_BTC_HOLDINGS, |
| 88 | + }, |
| 89 | + "ETH": { |
| 90 | + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_ETH_HOLDINGS, |
| 91 | + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_ETH_HOLDINGS, |
| 92 | + }, |
74 | 93 | } |
75 | 94 |
|
76 | 95 |
|
77 | 96 | async def _stub_get_all_currencies_price_ticker_no_network(self, symbols=None, **kwargs): |
78 | | - return {} |
| 97 | + if not symbols: |
| 98 | + return {} |
| 99 | + close_column = trading_enums.ExchangeConstantsTickersColumns.CLOSE.value |
| 100 | + tickers: dict[str, dict] = {} |
| 101 | + for symbol in symbols: |
| 102 | + if symbol == f"BTC/{_EXPECTED_PORTFOLIO_UNIT}": |
| 103 | + tickers[symbol] = {close_column: _FUNCTIONAL_BTC_USDT_CLOSE} |
| 104 | + return tickers |
79 | 105 |
|
80 | 106 |
|
81 | 107 | async def _run_user_action_to_completion( |
@@ -113,6 +139,31 @@ def _assert_listed_user_actions_match_expected_id_status_pairs( |
113 | 139 | assert actual_sorted == expected_sorted |
114 | 140 |
|
115 | 141 |
|
| 142 | +def _assert_functional_portfolio_content( |
| 143 | + portfolio_content: protocol_models.PortfolioContent | None, |
| 144 | +) -> None: |
| 145 | + assert portfolio_content is not None |
| 146 | + assert portfolio_content.unit == _EXPECTED_PORTFOLIO_UNIT |
| 147 | + assert portfolio_content.total == pytest.approx(_EXPECTED_PORTFOLIO_TOTAL) |
| 148 | + assets_by_symbol = {asset.symbol: asset for asset in portfolio_content.assets} |
| 149 | + assert set(assets_by_symbol) == {"USDT", "BTC", "ETH"} |
| 150 | + |
| 151 | + usdt_asset = assets_by_symbol["USDT"] |
| 152 | + assert usdt_asset.total == pytest.approx(_FUNCTIONAL_USDT_HOLDINGS) |
| 153 | + assert usdt_asset.available == pytest.approx(_FUNCTIONAL_USDT_HOLDINGS) |
| 154 | + assert usdt_asset.value == pytest.approx(_EXPECTED_USDT_VALUE) |
| 155 | + |
| 156 | + bitcoin_asset = assets_by_symbol["BTC"] |
| 157 | + assert bitcoin_asset.total == pytest.approx(_FUNCTIONAL_BTC_HOLDINGS) |
| 158 | + assert bitcoin_asset.available == pytest.approx(_FUNCTIONAL_BTC_HOLDINGS) |
| 159 | + assert bitcoin_asset.value == pytest.approx(_EXPECTED_BTC_VALUE) |
| 160 | + |
| 161 | + ethereum_asset = assets_by_symbol["ETH"] |
| 162 | + assert ethereum_asset.total == pytest.approx(_FUNCTIONAL_ETH_HOLDINGS) |
| 163 | + assert ethereum_asset.available == pytest.approx(_FUNCTIONAL_ETH_HOLDINGS) |
| 164 | + assert ethereum_asset.value == pytest.approx(_EXPECTED_ETH_VALUE) |
| 165 | + |
| 166 | + |
116 | 167 | @pytest.mark.asyncio |
117 | 168 | class TestExecuteUserActionAccountCrud: |
118 | 169 | async def test_create_edit_refresh_delete_accounts_on_temp_filesystem( |
@@ -343,6 +394,7 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco |
343 | 394 | assert persisted_created_account.state.status == protocol_models.AccountStatus.VALID |
344 | 395 | assert persisted_created_account.state.message == protocol_models.AccountStatusMessage.VALID |
345 | 396 | assert len(account_provider.list_items(wallet_address)) == 1 |
| 397 | + _assert_functional_portfolio_content(persisted_created_account.portfolio_content) |
346 | 398 |
|
347 | 399 | # Step 3 — Edit: enqueue workflow only first; poll listings mid retry before awaiting terminal output. |
348 | 400 | edited_account = build_account( |
@@ -442,6 +494,7 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco |
442 | 494 | assert persisted_refreshed_account.state is not None |
443 | 495 | assert persisted_refreshed_account.state.status == protocol_models.AccountStatus.VALID |
444 | 496 | assert persisted_refreshed_account.state.message == protocol_models.AccountStatusMessage.VALID |
| 497 | + _assert_functional_portfolio_content(persisted_refreshed_account.portfolio_content) |
445 | 498 |
|
446 | 499 | # Step 5 — Delete: remove account from provider; listing gains COMPLETED delete row. |
447 | 500 | delete_action = build_delete_user_action( |
|
0 commit comments