Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/project_x_py/trading_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
81 changes: 81 additions & 0 deletions tests/trading_suite/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down