Skip to content

Commit e61afed

Browse files
authored
feat(tests): add Order Manager test suite with comprehensive coverage and documentation
Co-authored-by: Genie <[email protected]> Add Comprehensive Testing Suite for Order Manager
2 parents f84d753 + 5be0267 commit e61afed

File tree

9 files changed

+481
-2
lines changed

9 files changed

+481
-2
lines changed

tests/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,15 @@ Areas for future test improvements:
7575
- Integration with CI/CD pipeline
7676
- Property-based testing for complex scenarios
7777
- Performance benchmarks
78-
- Snapshot testing for response structures
78+
- Snapshot testing for response structures
79+
80+
## Order Manager Tests
81+
82+
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.
83+
84+
**To run only Order Manager tests:**
85+
```bash
86+
pytest tests/order_manager/
87+
```
88+
89+
All network/API operations are mocked for speed and determinism. See `tests/order_manager/conftest.py` for local fixtures and helpers.

tests/TESTING.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,21 @@ Focus is placed on testing:
146146
The test suite is designed to integrate with CI/CD pipelines. Tests run automatically on:
147147
- Pull requests
148148
- Main branch changes
149-
- Release tags
149+
- Release tags
150+
151+
## Order Manager Tests
152+
153+
To run just the Order Manager test suite:
154+
155+
```bash
156+
pytest tests/order_manager/
157+
```
158+
159+
This suite covers:
160+
- `OrderManager` core API (place/search/cancel/modify)
161+
- Order type helpers (market, limit, stop, trailing-stop)
162+
- Bracket order validation and flows
163+
- Position order tracking and helpers
164+
- Utility price alignment functions
165+
166+
All network and API interactions are fully mocked using pytest and unittest.mock. Test execution is fast (<50ms per test).

tests/order_manager/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Order Manager test subpackage

tests/order_manager/conftest.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""OrderManager test-specific fixtures."""
2+
3+
import pytest
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
from project_x_py.models import Account, OrderPlaceResponse
7+
from project_x_py.order_manager.core import OrderManager
8+
9+
@pytest.fixture
10+
def order_manager(initialized_client):
11+
"""
12+
Fixture for an OrderManager wired to a mocked ProjectX client.
13+
Also patches align_price_to_tick_size to return the input price for determinism.
14+
"""
15+
# Patch align_price_to_tick_size in both utils and core to return input price for determinism
16+
patch_utils = patch(
17+
"project_x_py.order_manager.utils.align_price_to_tick_size",
18+
new=AsyncMock(side_effect=lambda price, *_args, **_kwargs: price),
19+
)
20+
patch_core = patch(
21+
"project_x_py.order_manager.core.align_price_to_tick_size",
22+
new=AsyncMock(side_effect=lambda price, *_args, **_kwargs: price),
23+
)
24+
patch_utils.start()
25+
patch_core.start()
26+
27+
# Set up a dummy account
28+
initialized_client.account_info = Account(
29+
id=12345,
30+
name="Test Account",
31+
balance=100000.0,
32+
canTrade=True,
33+
isVisible=True,
34+
simulated=True,
35+
)
36+
37+
om = OrderManager(initialized_client)
38+
yield om
39+
40+
patch_utils.stop()
41+
patch_core.stop()
42+
43+
44+
@pytest.fixture
45+
def make_order_response():
46+
"""
47+
Helper to build a dict compatible with OrderPlaceResponse.
48+
"""
49+
def _make(order_id, success=True, error_code=0, error_msg=None):
50+
return {
51+
"orderId": order_id,
52+
"success": success,
53+
"errorCode": error_code,
54+
"errorMessage": error_msg,
55+
}
56+
return _make
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Tests for BracketOrderMixin (validation and successful flows)."""
2+
3+
import pytest
4+
from unittest.mock import AsyncMock, patch
5+
6+
from project_x_py.models import OrderPlaceResponse, BracketOrderResponse
7+
from project_x_py.exceptions import ProjectXOrderError
8+
9+
@pytest.mark.asyncio
10+
class TestBracketOrderMixin:
11+
"""Unit tests for BracketOrderMixin bracket order placement."""
12+
13+
@pytest.mark.parametrize(
14+
"side, entry, stop, target, err",
15+
[
16+
(0, 100.0, 101.0, 102.0, "stop loss (101.0) must be below entry (100.0)"),
17+
(0, 100.0, 99.0, 99.0, "take profit (99.0) must be above entry (100.0)"),
18+
(1, 100.0, 99.0, 98.0, "stop loss (99.0) must be above entry (100.0)"),
19+
(1, 100.0, 101.0, 101.0, "take profit (101.0) must be below entry (100.0)"),
20+
]
21+
)
22+
async def test_bracket_order_validation_fails(self, side, entry, stop, target, err):
23+
"""BracketOrderMixin validates stop/take_profit price relationships."""
24+
from project_x_py.order_manager.bracket_orders import BracketOrderMixin
25+
mixin = BracketOrderMixin()
26+
mixin.place_market_order = AsyncMock()
27+
mixin.place_limit_order = AsyncMock()
28+
mixin.place_stop_order = AsyncMock()
29+
mixin.position_orders = {"FOO": {"entry_orders": [], "stop_orders": [], "target_orders": []}}
30+
mixin.stats = {"bracket_orders_placed": 0}
31+
with pytest.raises(ProjectXOrderError) as exc:
32+
await mixin.place_bracket_order(
33+
"FOO", side, 1, entry, stop, target, entry_type="limit"
34+
)
35+
assert err in str(exc.value)
36+
37+
async def test_bracket_order_success_flow(self):
38+
"""Successful bracket order path places all three orders and updates stats/caches."""
39+
from project_x_py.order_manager.bracket_orders import BracketOrderMixin
40+
mixin = BracketOrderMixin()
41+
mixin.place_market_order = AsyncMock(return_value=OrderPlaceResponse(orderId=1, success=True, errorCode=0, errorMessage=None))
42+
mixin.place_limit_order = AsyncMock(side_effect=[
43+
OrderPlaceResponse(orderId=2, success=True, errorCode=0, errorMessage=None),
44+
OrderPlaceResponse(orderId=3, success=True, errorCode=0, errorMessage=None),
45+
])
46+
mixin.place_stop_order = AsyncMock(return_value=OrderPlaceResponse(orderId=4, success=True, errorCode=0, errorMessage=None))
47+
mixin.position_orders = {"BAR": {"entry_orders": [], "stop_orders": [], "target_orders": []}}
48+
mixin.stats = {"bracket_orders_placed": 0}
49+
50+
# Entry type = limit
51+
resp = await mixin.place_bracket_order(
52+
"BAR", 0, 2, 100.0, 99.0, 103.0, entry_type="limit"
53+
)
54+
assert isinstance(resp, BracketOrderResponse)
55+
assert resp.success
56+
assert resp.entry_order_id == 2
57+
assert resp.stop_order_id == 4
58+
assert resp.target_order_id == 3
59+
assert mixin.position_orders["BAR"]["entry_orders"][-1] == 2
60+
assert mixin.position_orders["BAR"]["stop_orders"][-1] == 4
61+
assert mixin.position_orders["BAR"]["target_orders"][-1] == 3
62+
assert mixin.stats["bracket_orders_placed"] == 1

tests/order_manager/test_core.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Tests for OrderManager core API."""
2+
3+
import pytest
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
from project_x_py.models import Order, OrderPlaceResponse
7+
from project_x_py.exceptions import ProjectXOrderError
8+
9+
10+
class TestOrderManagerCore:
11+
"""Unit tests for OrderManager core public methods."""
12+
13+
@pytest.mark.asyncio
14+
async def test_place_market_order_success(self, order_manager, make_order_response):
15+
"""place_market_order hits /Order/place with correct payload and updates stats."""
16+
# Patch _make_request to return a success response
17+
order_manager.project_x._make_request = AsyncMock(
18+
return_value=make_order_response(42)
19+
)
20+
# Should increment orders_placed
21+
start_count = order_manager.stats["orders_placed"]
22+
resp = await order_manager.place_market_order("MGC", 0, 2)
23+
assert isinstance(resp, OrderPlaceResponse)
24+
assert resp.orderId == 42
25+
assert order_manager.project_x._make_request.call_count == 1
26+
call_args = order_manager.project_x._make_request.call_args[1]["data"]
27+
assert call_args["contractId"] == "MGC"
28+
assert call_args["type"] == 2
29+
assert call_args["side"] == 0
30+
assert call_args["size"] == 2
31+
assert order_manager.stats["orders_placed"] == start_count + 1
32+
33+
@pytest.mark.asyncio
34+
async def test_place_order_error_raises(self, order_manager, make_order_response):
35+
"""place_order raises ProjectXOrderError when API fails."""
36+
order_manager.project_x._make_request = AsyncMock(
37+
return_value={"success": False, "errorMessage": "Test error"}
38+
)
39+
with pytest.raises(ProjectXOrderError):
40+
await order_manager.place_order("MGC", 2, 0, 1)
41+
42+
@pytest.mark.asyncio
43+
async def test_search_open_orders_populates_cache(self, order_manager, make_order_response):
44+
"""search_open_orders converts API dicts to Order objects and populates cache."""
45+
resp_order = {
46+
"id": 101, "accountId": 12345, "contractId": "MGC",
47+
"creationTimestamp": "2024-01-01T01:00:00Z", "updateTimestamp": None,
48+
"status": 1, "type": 1, "side": 0, "size": 2
49+
}
50+
order_manager.project_x.account_info.id = 12345
51+
order_manager.project_x._make_request = AsyncMock(
52+
return_value={"success": True, "orders": [resp_order]}
53+
)
54+
orders = await order_manager.search_open_orders()
55+
assert isinstance(orders[0], Order)
56+
assert order_manager.tracked_orders[str(resp_order["id"])] == resp_order
57+
assert order_manager.order_status_cache[str(resp_order["id"])] == 1
58+
59+
@pytest.mark.asyncio
60+
async def test_is_order_filled_cache_hit(self, order_manager):
61+
"""is_order_filled returns True from cache and does not call _make_request if cached."""
62+
order_manager._realtime_enabled = True
63+
order_manager.order_status_cache["77"] = 2 # 2=Filled
64+
order_manager.project_x._make_request = AsyncMock()
65+
result = await order_manager.is_order_filled(77)
66+
assert result is True
67+
order_manager.project_x._make_request.assert_not_called()
68+
69+
@pytest.mark.asyncio
70+
async def test_is_order_filled_fallback(self, order_manager):
71+
"""is_order_filled falls back to get_order_by_id when not cached."""
72+
order_manager._realtime_enabled = False
73+
dummy_order = Order(
74+
id=55, accountId=12345, contractId="CL",
75+
creationTimestamp="2024-01-01T01:00:00Z", updateTimestamp=None,
76+
status=2, type=1, side=0, size=1
77+
)
78+
order_manager.get_order_by_id = AsyncMock(return_value=dummy_order)
79+
result = await order_manager.is_order_filled(55)
80+
assert result is True
81+
82+
@pytest.mark.asyncio
83+
async def test_cancel_order_success_and_failure(self, order_manager):
84+
"""cancel_order updates caches/stats on success and handles failure."""
85+
# Setup tracked order
86+
order_manager.tracked_orders["888"] = {"status": 1}
87+
order_manager.order_status_cache["888"] = 1
88+
start = order_manager.stats["orders_cancelled"]
89+
order_manager.project_x._make_request = AsyncMock(return_value={"success": True})
90+
assert await order_manager.cancel_order(888) is True
91+
assert order_manager.tracked_orders["888"]["status"] == 3
92+
assert order_manager.order_status_cache["888"] == 3
93+
assert order_manager.stats["orders_cancelled"] == start + 1
94+
95+
order_manager.project_x._make_request = AsyncMock(return_value={"success": False, "errorMessage": "fail"})
96+
assert await order_manager.cancel_order(888) is False
97+
98+
@pytest.mark.asyncio
99+
async def test_modify_order_success_and_aligns(self, order_manager):
100+
"""modify_order aligns prices, makes API call, returns True on success."""
101+
dummy_order = Order(
102+
id=12, accountId=12345, contractId="MGC",
103+
creationTimestamp="2024-01-01T01:00:00Z", updateTimestamp=None,
104+
status=1, type=1, side=0, size=1
105+
)
106+
order_manager.get_order_by_id = AsyncMock(return_value=dummy_order)
107+
order_manager.project_x._make_request = AsyncMock(return_value={"success": True})
108+
assert await order_manager.modify_order(12, limit_price=2000.5) is True
109+
110+
order_manager.project_x._make_request = AsyncMock(return_value={"success": False})
111+
assert await order_manager.modify_order(12, limit_price=2001.5) is False
112+
113+
@pytest.mark.asyncio
114+
async def test_get_order_statistics(self, order_manager):
115+
"""get_order_statistics returns expected health_status key and stats."""
116+
stats = await order_manager.get_order_statistics()
117+
assert "statistics" in stats
118+
assert "health_status" in stats
119+
assert stats["health_status"] in {"healthy", "degraded"}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Tests for OrderTypesMixin helpers (market/limit/stop/trailing-stop)."""
2+
3+
import pytest
4+
from unittest.mock import AsyncMock
5+
6+
from project_x_py.models import OrderPlaceResponse
7+
8+
class DummyOrderManager:
9+
def __init__(self):
10+
self.place_order = AsyncMock()
11+
12+
@pytest.mark.asyncio
13+
class TestOrderTypesMixin:
14+
"""Unit tests for OrderTypesMixin order type wrappers."""
15+
16+
async def test_place_market_order(self):
17+
"""place_market_order delegates to place_order with order_type=2."""
18+
dummy = DummyOrderManager()
19+
from project_x_py.order_manager.order_types import OrderTypesMixin
20+
mixin = OrderTypesMixin()
21+
mixin.place_order = dummy.place_order
22+
await mixin.place_market_order("MGC", 0, 1)
23+
dummy.place_order.assert_awaited_once()
24+
args = dummy.place_order.call_args.kwargs
25+
assert args["order_type"] == 2
26+
27+
async def test_place_limit_order(self):
28+
"""place_limit_order delegates to place_order with order_type=1 and passes limit_price."""
29+
dummy = DummyOrderManager()
30+
from project_x_py.order_manager.order_types import OrderTypesMixin
31+
mixin = OrderTypesMixin()
32+
mixin.place_order = dummy.place_order
33+
await mixin.place_limit_order("MGC", 1, 2, 2040.0)
34+
args = dummy.place_order.call_args.kwargs
35+
assert args["order_type"] == 1
36+
assert args["limit_price"] == 2040.0
37+
38+
async def test_place_stop_order(self):
39+
"""place_stop_order delegates to place_order with order_type=4 and passes stop_price."""
40+
dummy = DummyOrderManager()
41+
from project_x_py.order_manager.order_types import OrderTypesMixin
42+
mixin = OrderTypesMixin()
43+
mixin.place_order = dummy.place_order
44+
await mixin.place_stop_order("MGC", 1, 2, 2030.0)
45+
args = dummy.place_order.call_args.kwargs
46+
assert args["order_type"] == 4
47+
assert args["stop_price"] == 2030.0
48+
49+
async def test_place_trailing_stop_order(self):
50+
"""place_trailing_stop_order delegates to place_order with order_type=5 and passes trail_price."""
51+
dummy = DummyOrderManager()
52+
from project_x_py.order_manager.order_types import OrderTypesMixin
53+
mixin = OrderTypesMixin()
54+
mixin.place_order = dummy.place_order
55+
await mixin.place_trailing_stop_order("MGC", 1, 2, 5.0)
56+
args = dummy.place_order.call_args.kwargs
57+
assert args["order_type"] == 5
58+
assert args["trail_price"] == 5.0

0 commit comments

Comments
 (0)