From 8031327c392a8fe286feba9304cd6cec520f4a1e Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Sun, 28 Jun 2026 20:13:07 -0700 Subject: [PATCH] support direct credentials in TradingSuite.create --- src/project_x_py/trading_suite.py | 23 ++++++++- tests/trading_suite/test_core.py | 81 +++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/project_x_py/trading_suite.py b/src/project_x_py/trading_suite.py index f1bf151..168a7c5 100644 --- a/src/project_x_py/trading_suite.py +++ b/src/project_x_py/trading_suite.py @@ -471,6 +471,9 @@ async def create( timeframes: Data timeframes (default: ["5min"]) features: Optional features to enable session_config: Optional session configuration + username: Optional ProjectX username. Must be used with api_key. + api_key: Optional ProjectX API key. Must be used with username. + account_name: Optional account name to select during authentication **kwargs: Additional configuration options Returns: @@ -507,6 +510,16 @@ async def create( "Must provide either 'instruments' or 'instrument' parameter" ) + username = cast(str | None, kwargs.pop("username", None)) + api_key = cast(str | None, kwargs.pop("api_key", None)) + account_name = cast(str | None, kwargs.pop("account_name", None)) + + if (username is None) != (api_key is None): + raise ValueError( + "Both 'username' and 'api_key' must be provided for direct " + "TradingSuite authentication" + ) + # Build configuration using primary instrument config = TradingSuiteConfig( instrument=primary_instrument, @@ -517,7 +530,15 @@ async def create( ) # Create and authenticate client - client_context = ProjectX.from_env() + client_context: AbstractAsyncContextManager[ProjectXBase] + if username is not None and api_key is not None: + client_context = ProjectX( + username=username, + api_key=api_key, + account_name=account_name.upper() if account_name else None, + ) + else: + client_context = ProjectX.from_env(account_name=account_name) client = await client_context.__aenter__() try: diff --git a/tests/trading_suite/test_core.py b/tests/trading_suite/test_core.py index 07fa799..9d11572 100644 --- a/tests/trading_suite/test_core.py +++ b/tests/trading_suite/test_core.py @@ -12,6 +12,25 @@ from project_x_py.models import Account +def _mock_authenticated_client() -> MagicMock: + mock_client = MagicMock() + mock_client.account_info = Account( + id=12345, + name="TEST_ACCOUNT", + balance=100000.0, + canTrade=True, + isVisible=True, + simulated=True, + ) + mock_client.session_token = "mock_jwt_token" + mock_client.config = MagicMock() + mock_client.authenticate = AsyncMock() + mock_client.get_instrument = AsyncMock(return_value=MagicMock(id="MNQ_CONTRACT_ID")) + mock_client.search_all_orders = AsyncMock(return_value=[]) + mock_client.search_open_positions = AsyncMock(return_value=[]) + return mock_client + + @pytest.mark.asyncio async def test_trading_suite_create(): """Test basic TradingSuite creation with mocked client.""" @@ -125,6 +144,68 @@ async def test_trading_suite_create(): assert suite._initialized is False +@pytest.mark.asyncio +async def test_trading_suite_create_with_direct_credentials(): + """Test TradingSuite creation with directly supplied ProjectX credentials.""" + + mock_client = _mock_authenticated_client() + + mock_context = AsyncMock() + mock_context.__aenter__.return_value = mock_client + mock_context.__aexit__.return_value = None + + mock_realtime = MagicMock() + mock_realtime.disconnect = AsyncMock(return_value=None) + + mock_data_manager = MagicMock() + mock_data_manager.stop_realtime_feed = AsyncMock(return_value=None) + mock_data_manager.cleanup = AsyncMock(return_value=None) + + mock_position_manager = MagicMock() + + with patch( + "project_x_py.trading_suite.ProjectX", return_value=mock_context + ) as mock_project_x: + with patch( + "project_x_py.trading_suite.ProjectXRealtimeClient", + return_value=mock_realtime, + ): + with patch( + "project_x_py.trading_suite.RealtimeDataManager", + return_value=mock_data_manager, + ): + with patch( + "project_x_py.trading_suite.PositionManager", + return_value=mock_position_manager, + ): + suite = await TradingSuite.create( + "MNQ", + username="direct_user", + api_key="direct_key", + account_name="test_account", + auto_connect=False, + ) + + mock_project_x.assert_called_once_with( + username="direct_user", + api_key="direct_key", + account_name="TEST_ACCOUNT", + ) + assert suite.client == mock_client + assert suite.config.auto_connect is False + + await suite.disconnect() + mock_context.__aexit__.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_trading_suite_create_requires_direct_credentials_together(): + """Test direct credentials fail early when partially supplied.""" + + with pytest.raises(ValueError, match="Both 'username' and 'api_key'"): + await TradingSuite.create("MNQ", username="direct_user") + + @pytest.mark.asyncio async def test_trading_suite_with_features(): """Test TradingSuite creation with optional features."""