Skip to content
Merged
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
13 changes: 12 additions & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,15 @@ Areas for future test improvements:
- Integration with CI/CD pipeline
- Property-based testing for complex scenarios
- Performance benchmarks
- Snapshot testing for response structures
- Snapshot testing for response structures

## Order Manager Tests

The `order_manager` test suite provides high-coverage, unit-level tests for all major flows in `src/project_x_py/order_manager/`. Coverage includes order placement, bracket/position helpers, utils, and price alignment logic.

**To run only Order Manager tests:**
```bash
pytest tests/order_manager/
```

All network/API operations are mocked for speed and determinism. See `tests/order_manager/conftest.py` for local fixtures and helpers.
19 changes: 18 additions & 1 deletion tests/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,21 @@ Focus is placed on testing:
The test suite is designed to integrate with CI/CD pipelines. Tests run automatically on:
- Pull requests
- Main branch changes
- Release tags
- Release tags

## Order Manager Tests

To run just the Order Manager test suite:

```bash
pytest tests/order_manager/
```

This suite covers:
- `OrderManager` core API (place/search/cancel/modify)
- Order type helpers (market, limit, stop, trailing-stop)
- Bracket order validation and flows
- Position order tracking and helpers
- Utility price alignment functions

All network and API interactions are fully mocked using pytest and unittest.mock. Test execution is fast (<50ms per test).
1 change: 1 addition & 0 deletions tests/order_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Order Manager test subpackage
56 changes: 56 additions & 0 deletions tests/order_manager/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""OrderManager test-specific fixtures."""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from project_x_py.models import Account, OrderPlaceResponse
from project_x_py.order_manager.core import OrderManager

@pytest.fixture
def order_manager(initialized_client):
"""
Fixture for an OrderManager wired to a mocked ProjectX client.
Also patches align_price_to_tick_size to return the input price for determinism.
"""
# Patch align_price_to_tick_size in both utils and core to return input price for determinism
patch_utils = patch(
"project_x_py.order_manager.utils.align_price_to_tick_size",
new=AsyncMock(side_effect=lambda price, *_args, **_kwargs: price),
)
patch_core = patch(
"project_x_py.order_manager.core.align_price_to_tick_size",
new=AsyncMock(side_effect=lambda price, *_args, **_kwargs: price),
)
patch_utils.start()
patch_core.start()

# Set up a dummy account
initialized_client.account_info = Account(
id=12345,
name="Test Account",
balance=100000.0,
canTrade=True,
isVisible=True,
simulated=True,
)

om = OrderManager(initialized_client)
yield om

patch_utils.stop()
patch_core.stop()


@pytest.fixture
def make_order_response():
"""
Helper to build a dict compatible with OrderPlaceResponse.
"""
def _make(order_id, success=True, error_code=0, error_msg=None):
return {
"orderId": order_id,
"success": success,
"errorCode": error_code,
"errorMessage": error_msg,
}
return _make
62 changes: 62 additions & 0 deletions tests/order_manager/test_bracket_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Tests for BracketOrderMixin (validation and successful flows)."""

import pytest
from unittest.mock import AsyncMock, patch

from project_x_py.models import OrderPlaceResponse, BracketOrderResponse
from project_x_py.exceptions import ProjectXOrderError

@pytest.mark.asyncio
class TestBracketOrderMixin:
"""Unit tests for BracketOrderMixin bracket order placement."""

@pytest.mark.parametrize(
"side, entry, stop, target, err",
[
(0, 100.0, 101.0, 102.0, "stop loss (101.0) must be below entry (100.0)"),
(0, 100.0, 99.0, 99.0, "take profit (99.0) must be above entry (100.0)"),
(1, 100.0, 99.0, 98.0, "stop loss (99.0) must be above entry (100.0)"),
(1, 100.0, 101.0, 101.0, "take profit (101.0) must be below entry (100.0)"),
]
)
async def test_bracket_order_validation_fails(self, side, entry, stop, target, err):
"""BracketOrderMixin validates stop/take_profit price relationships."""
from project_x_py.order_manager.bracket_orders import BracketOrderMixin
mixin = BracketOrderMixin()
mixin.place_market_order = AsyncMock()
mixin.place_limit_order = AsyncMock()
mixin.place_stop_order = AsyncMock()
mixin.position_orders = {"FOO": {"entry_orders": [], "stop_orders": [], "target_orders": []}}
mixin.stats = {"bracket_orders_placed": 0}
with pytest.raises(ProjectXOrderError) as exc:
await mixin.place_bracket_order(
"FOO", side, 1, entry, stop, target, entry_type="limit"
)
assert err in str(exc.value)

async def test_bracket_order_success_flow(self):
"""Successful bracket order path places all three orders and updates stats/caches."""
from project_x_py.order_manager.bracket_orders import BracketOrderMixin
mixin = BracketOrderMixin()
mixin.place_market_order = AsyncMock(return_value=OrderPlaceResponse(orderId=1, success=True, errorCode=0, errorMessage=None))
mixin.place_limit_order = AsyncMock(side_effect=[
OrderPlaceResponse(orderId=2, success=True, errorCode=0, errorMessage=None),
OrderPlaceResponse(orderId=3, success=True, errorCode=0, errorMessage=None),
])
mixin.place_stop_order = AsyncMock(return_value=OrderPlaceResponse(orderId=4, success=True, errorCode=0, errorMessage=None))
mixin.position_orders = {"BAR": {"entry_orders": [], "stop_orders": [], "target_orders": []}}
mixin.stats = {"bracket_orders_placed": 0}

# Entry type = limit
resp = await mixin.place_bracket_order(
"BAR", 0, 2, 100.0, 99.0, 103.0, entry_type="limit"
)
assert isinstance(resp, BracketOrderResponse)
assert resp.success
assert resp.entry_order_id == 2
assert resp.stop_order_id == 4
assert resp.target_order_id == 3
assert mixin.position_orders["BAR"]["entry_orders"][-1] == 2
assert mixin.position_orders["BAR"]["stop_orders"][-1] == 4
assert mixin.position_orders["BAR"]["target_orders"][-1] == 3
assert mixin.stats["bracket_orders_placed"] == 1
119 changes: 119 additions & 0 deletions tests/order_manager/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Tests for OrderManager core API."""

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from project_x_py.models import Order, OrderPlaceResponse
from project_x_py.exceptions import ProjectXOrderError


class TestOrderManagerCore:
"""Unit tests for OrderManager core public methods."""

@pytest.mark.asyncio
async def test_place_market_order_success(self, order_manager, make_order_response):
"""place_market_order hits /Order/place with correct payload and updates stats."""
# Patch _make_request to return a success response
order_manager.project_x._make_request = AsyncMock(
return_value=make_order_response(42)
)
# Should increment orders_placed
start_count = order_manager.stats["orders_placed"]
resp = await order_manager.place_market_order("MGC", 0, 2)
assert isinstance(resp, OrderPlaceResponse)
assert resp.orderId == 42
assert order_manager.project_x._make_request.call_count == 1
call_args = order_manager.project_x._make_request.call_args[1]["data"]
assert call_args["contractId"] == "MGC"
assert call_args["type"] == 2
assert call_args["side"] == 0
assert call_args["size"] == 2
assert order_manager.stats["orders_placed"] == start_count + 1

@pytest.mark.asyncio
async def test_place_order_error_raises(self, order_manager, make_order_response):
"""place_order raises ProjectXOrderError when API fails."""
order_manager.project_x._make_request = AsyncMock(
return_value={"success": False, "errorMessage": "Test error"}
)
with pytest.raises(ProjectXOrderError):
await order_manager.place_order("MGC", 2, 0, 1)

@pytest.mark.asyncio
async def test_search_open_orders_populates_cache(self, order_manager, make_order_response):
"""search_open_orders converts API dicts to Order objects and populates cache."""
resp_order = {
"id": 101, "accountId": 12345, "contractId": "MGC",
"creationTimestamp": "2024-01-01T01:00:00Z", "updateTimestamp": None,
"status": 1, "type": 1, "side": 0, "size": 2
}
order_manager.project_x.account_info.id = 12345
order_manager.project_x._make_request = AsyncMock(
return_value={"success": True, "orders": [resp_order]}
)
orders = await order_manager.search_open_orders()
assert isinstance(orders[0], Order)
assert order_manager.tracked_orders[str(resp_order["id"])] == resp_order
assert order_manager.order_status_cache[str(resp_order["id"])] == 1

@pytest.mark.asyncio
async def test_is_order_filled_cache_hit(self, order_manager):
"""is_order_filled returns True from cache and does not call _make_request if cached."""
order_manager._realtime_enabled = True
order_manager.order_status_cache["77"] = 2 # 2=Filled
order_manager.project_x._make_request = AsyncMock()
result = await order_manager.is_order_filled(77)
assert result is True
order_manager.project_x._make_request.assert_not_called()

@pytest.mark.asyncio
async def test_is_order_filled_fallback(self, order_manager):
"""is_order_filled falls back to get_order_by_id when not cached."""
order_manager._realtime_enabled = False
dummy_order = Order(
id=55, accountId=12345, contractId="CL",
creationTimestamp="2024-01-01T01:00:00Z", updateTimestamp=None,
status=2, type=1, side=0, size=1
)
order_manager.get_order_by_id = AsyncMock(return_value=dummy_order)
result = await order_manager.is_order_filled(55)
assert result is True

@pytest.mark.asyncio
async def test_cancel_order_success_and_failure(self, order_manager):
"""cancel_order updates caches/stats on success and handles failure."""
# Setup tracked order
order_manager.tracked_orders["888"] = {"status": 1}
order_manager.order_status_cache["888"] = 1
start = order_manager.stats["orders_cancelled"]
order_manager.project_x._make_request = AsyncMock(return_value={"success": True})
assert await order_manager.cancel_order(888) is True
assert order_manager.tracked_orders["888"]["status"] == 3
assert order_manager.order_status_cache["888"] == 3
assert order_manager.stats["orders_cancelled"] == start + 1

order_manager.project_x._make_request = AsyncMock(return_value={"success": False, "errorMessage": "fail"})
assert await order_manager.cancel_order(888) is False

@pytest.mark.asyncio
async def test_modify_order_success_and_aligns(self, order_manager):
"""modify_order aligns prices, makes API call, returns True on success."""
dummy_order = Order(
id=12, accountId=12345, contractId="MGC",
creationTimestamp="2024-01-01T01:00:00Z", updateTimestamp=None,
status=1, type=1, side=0, size=1
)
order_manager.get_order_by_id = AsyncMock(return_value=dummy_order)
order_manager.project_x._make_request = AsyncMock(return_value={"success": True})
assert await order_manager.modify_order(12, limit_price=2000.5) is True

order_manager.project_x._make_request = AsyncMock(return_value={"success": False})
assert await order_manager.modify_order(12, limit_price=2001.5) is False

@pytest.mark.asyncio
async def test_get_order_statistics(self, order_manager):
"""get_order_statistics returns expected health_status key and stats."""
stats = await order_manager.get_order_statistics()
assert "statistics" in stats
assert "health_status" in stats
assert stats["health_status"] in {"healthy", "degraded"}
58 changes: 58 additions & 0 deletions tests/order_manager/test_order_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Tests for OrderTypesMixin helpers (market/limit/stop/trailing-stop)."""

import pytest
from unittest.mock import AsyncMock

from project_x_py.models import OrderPlaceResponse

class DummyOrderManager:
def __init__(self):
self.place_order = AsyncMock()

@pytest.mark.asyncio
class TestOrderTypesMixin:
"""Unit tests for OrderTypesMixin order type wrappers."""

async def test_place_market_order(self):
"""place_market_order delegates to place_order with order_type=2."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin
mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_market_order("MGC", 0, 1)
dummy.place_order.assert_awaited_once()
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 2

async def test_place_limit_order(self):
"""place_limit_order delegates to place_order with order_type=1 and passes limit_price."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin
mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_limit_order("MGC", 1, 2, 2040.0)
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 1
assert args["limit_price"] == 2040.0

async def test_place_stop_order(self):
"""place_stop_order delegates to place_order with order_type=4 and passes stop_price."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin
mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_stop_order("MGC", 1, 2, 2030.0)
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 4
assert args["stop_price"] == 2030.0

async def test_place_trailing_stop_order(self):
"""place_trailing_stop_order delegates to place_order with order_type=5 and passes trail_price."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin
mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_trailing_stop_order("MGC", 1, 2, 5.0)
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 5
assert args["trail_price"] == 5.0
Loading