Skip to content

Commit 5cc8e92

Browse files
authored
Merge pull request #27 from TexasCoding/order_types
feat: add JoinBid and JoinAsk order types
2 parents 0256a6f + 317854a commit 5cc8e92

File tree

9 files changed

+255
-8
lines changed

9 files changed

+255
-8
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Old implementations are removed when improved
1414
- Clean, modern code architecture is prioritized
1515

16+
## [2.0.7] - 2025-08-03
17+
18+
### Added
19+
- **📈 JoinBid and JoinAsk Order Types**: Passive liquidity-providing order types
20+
- `place_join_bid_order()`: Places limit buy order at current best bid price
21+
- `place_join_ask_order()`: Places limit sell order at current best ask price
22+
- These order types automatically join the best bid/ask queue
23+
- Useful for market making strategies and minimizing market impact
24+
- Added comprehensive tests for both order types
25+
- Created example script `16_join_orders.py` demonstrating usage
26+
27+
### Improved
28+
- **📖 Order Type Documentation**: Enhanced documentation for all order types
29+
- Clarified that JoinBid/JoinAsk are passive orders, not stop-limit orders
30+
- Updated order type enum documentation with behavior descriptions
31+
- Added inline comments explaining each order type value
32+
1633
## [2.0.6] - 2025-08-03
1734

1835
### Changed

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
project = "project-x-py"
2424
copyright = "2025, Jeff West"
2525
author = "Jeff West"
26-
release = "2.0.6"
27-
version = "2.0.6"
26+
release = "2.0.7"
27+
version = "2.0.7"
2828

2929
# -- General configuration ---------------------------------------------------
3030

examples/16_join_orders.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating JoinBid and JoinAsk order types.
4+
5+
JoinBid and JoinAsk orders are passive liquidity-providing orders that automatically
6+
place limit orders at the current best bid or ask price. They're useful for:
7+
- Market making strategies
8+
- Providing liquidity
9+
- Minimizing market impact
10+
- Getting favorable queue position
11+
"""
12+
13+
import asyncio
14+
import os
15+
import sys
16+
from pathlib import Path
17+
18+
# Add src to Python path for development
19+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
20+
21+
from project_x_py import ProjectX, create_order_manager
22+
23+
24+
async def main():
25+
"""Demonstrate JoinBid and JoinAsk order placement."""
26+
# Initialize client
27+
async with ProjectX.from_env() as client:
28+
await client.authenticate()
29+
30+
# Create order manager
31+
order_manager = create_order_manager(client)
32+
33+
# Contract to trade
34+
contract = "MNQ"
35+
36+
print(f"=== JoinBid and JoinAsk Order Example for {contract} ===\n")
37+
38+
# Get current market data to show context
39+
bars = await client.get_bars(contract, days=1, timeframe="1min")
40+
if bars and not bars.is_empty():
41+
latest = bars.tail(1)
42+
print(f"Current market context:")
43+
print(f" Last price: ${latest['close'][0]:,.2f}")
44+
print(f" High: ${latest['high'][0]:,.2f}")
45+
print(f" Low: ${latest['low'][0]:,.2f}\n")
46+
47+
try:
48+
# Example 1: Place a JoinBid order
49+
print("1. Placing JoinBid order (buy at best bid)...")
50+
join_bid_response = await order_manager.place_join_bid_order(
51+
contract_id=contract, size=1
52+
)
53+
54+
if join_bid_response.success:
55+
print(f"✅ JoinBid order placed successfully!")
56+
print(f" Order ID: {join_bid_response.orderId}")
57+
print(f" This order will buy at the current best bid price\n")
58+
else:
59+
print(f"❌ JoinBid order failed: {join_bid_response.message}\n")
60+
61+
# Wait a moment
62+
await asyncio.sleep(2)
63+
64+
# Example 2: Place a JoinAsk order
65+
print("2. Placing JoinAsk order (sell at best ask)...")
66+
join_ask_response = await order_manager.place_join_ask_order(
67+
contract_id=contract, size=1
68+
)
69+
70+
if join_ask_response.success:
71+
print(f"✅ JoinAsk order placed successfully!")
72+
print(f" Order ID: {join_ask_response.orderId}")
73+
print(f" This order will sell at the current best ask price\n")
74+
else:
75+
print(f"❌ JoinAsk order failed: {join_ask_response.message}\n")
76+
77+
# Show order status
78+
print("3. Checking order status...")
79+
active_orders = await order_manager.get_active_orders()
80+
81+
print(f"\nActive orders: {len(active_orders)}")
82+
for order in active_orders:
83+
if order.id in [join_bid_response.orderId, join_ask_response.orderId]:
84+
order_type = "JoinBid" if order.side == 0 else "JoinAsk"
85+
side = "Buy" if order.side == 0 else "Sell"
86+
print(
87+
f" - {order_type} Order {order.id}: {side} {order.size} @ ${order.price:,.2f}"
88+
)
89+
90+
# Cancel orders to clean up
91+
print("\n4. Cancelling orders...")
92+
if join_bid_response.success:
93+
cancel_result = await order_manager.cancel_order(
94+
join_bid_response.orderId
95+
)
96+
if cancel_result.success:
97+
print(f"✅ JoinBid order {join_bid_response.orderId} cancelled")
98+
99+
if join_ask_response.success:
100+
cancel_result = await order_manager.cancel_order(
101+
join_ask_response.orderId
102+
)
103+
if cancel_result.success:
104+
print(f"✅ JoinAsk order {join_ask_response.orderId} cancelled")
105+
106+
except Exception as e:
107+
print(f"❌ Error: {e}")
108+
109+
print("\n=== JoinBid/JoinAsk Example Complete ===")
110+
print("\nKey Points:")
111+
print("- JoinBid places a limit buy order at the current best bid")
112+
print("- JoinAsk places a limit sell order at the current best ask")
113+
print("- These are passive orders that provide liquidity")
114+
print("- The actual fill price depends on market conditions")
115+
print("- Useful for market making and minimizing market impact")
116+
117+
118+
if __name__ == "__main__":
119+
# Check for required environment variables
120+
if not os.getenv("PROJECT_X_API_KEY") or not os.getenv("PROJECT_X_USERNAME"):
121+
print(
122+
"❌ Error: Please set PROJECT_X_API_KEY and PROJECT_X_USERNAME environment variables"
123+
)
124+
print("Example:")
125+
print(' export PROJECT_X_API_KEY="your-api-key"')
126+
print(' export PROJECT_X_USERNAME="your-username"')
127+
sys.exit(1)
128+
129+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "project-x-py"
3-
version = "2.0.6"
3+
version = "2.0.7"
44
description = "High-performance Python SDK for futures trading with real-time WebSocket data, technical indicators, order management, and market depth analysis"
55
readme = "README.md"
66
license = { text = "MIT" }

src/project_x_py/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797

9898
from project_x_py.client.base import ProjectXBase
9999

100-
__version__ = "2.0.6"
100+
__version__ = "2.0.7"
101101
__author__ = "TexasCoding"
102102

103103
# Core client classes - renamed from Async* to standard names

src/project_x_py/indicators/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
)
191191

192192
# Version info
193-
__version__ = "2.0.6"
193+
__version__ = "2.0.7"
194194
__author__ = "TexasCoding"
195195

196196

src/project_x_py/order_manager/core.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ class OrderManager(
104104
Order Type Enum Values:
105105
- 1: Limit
106106
- 2: Market
107+
- 3: StopLimit
107108
- 4: Stop
108109
- 5: TrailingStop
109-
- 6: JoinBid
110-
- 7: JoinAsk
110+
- 6: JoinBid (places limit buy at current best bid)
111+
- 7: JoinAsk (places limit sell at current best ask)
111112
112113
The OrderManager combines multiple mixins to provide a unified interface for all
113114
order-related operations, ensuring consistent behavior and comprehensive functionality

src/project_x_py/order_manager/order_types.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
- Limit Orders: Execution at specified price or better
2323
- Stop Orders: Market orders triggered at stop price
2424
- Trailing Stop Orders: Dynamic stops that follow price movement
25+
- Join Bid Orders: Limit buy orders at current best bid price
26+
- Join Ask Orders: Limit sell orders at current best ask price
2527
2628
Each order type method provides a simplified interface for common order placement
2729
scenarios while maintaining full compatibility with the underlying order system.
@@ -33,6 +35,8 @@
3335
await om.place_market_order("MGC", 0, 1)
3436
await om.place_stop_order("MGC", 1, 1, 2040.0)
3537
await om.place_trailing_stop_order("MGC", 1, 1, 5.0)
38+
await om.place_join_bid_order("MGC", 1) # Join bid side
39+
await om.place_join_ask_order("MGC", 1) # Join ask side
3640
```
3741
3842
See Also:
@@ -45,7 +49,7 @@
4549
from typing import TYPE_CHECKING
4650

4751
from project_x_py.models import OrderPlaceResponse
48-
from project_x_py.types.trading import OrderType
52+
from project_x_py.types.trading import OrderSide, OrderType
4953

5054
if TYPE_CHECKING:
5155
from project_x_py.types import OrderManagerProtocol
@@ -194,3 +198,71 @@ async def place_trailing_stop_order(
194198
trail_price=trail_price,
195199
account_id=account_id,
196200
)
201+
202+
async def place_join_bid_order(
203+
self: "OrderManagerProtocol",
204+
contract_id: str,
205+
size: int,
206+
account_id: int | None = None,
207+
) -> OrderPlaceResponse:
208+
"""
209+
Place a join bid order (limit order at current best bid price).
210+
211+
Join bid orders automatically place a limit buy order at the current
212+
best bid price, joining the queue of passive liquidity providers.
213+
The order will be placed at whatever the best bid price is at the
214+
time of submission.
215+
216+
Args:
217+
contract_id: The contract ID to trade
218+
size: Number of contracts to trade
219+
account_id: Account ID. Uses default account if None.
220+
221+
Returns:
222+
OrderPlaceResponse: Response containing order ID and status
223+
224+
Example:
225+
>>> # Join the bid to provide liquidity
226+
>>> response = await order_manager.place_join_bid_order("MGC", 1)
227+
"""
228+
return await self.place_order(
229+
contract_id=contract_id,
230+
side=OrderSide.BUY,
231+
size=size,
232+
order_type=OrderType.JOIN_BID,
233+
account_id=account_id,
234+
)
235+
236+
async def place_join_ask_order(
237+
self: "OrderManagerProtocol",
238+
contract_id: str,
239+
size: int,
240+
account_id: int | None = None,
241+
) -> OrderPlaceResponse:
242+
"""
243+
Place a join ask order (limit order at current best ask price).
244+
245+
Join ask orders automatically place a limit sell order at the current
246+
best ask price, joining the queue of passive liquidity providers.
247+
The order will be placed at whatever the best ask price is at the
248+
time of submission.
249+
250+
Args:
251+
contract_id: The contract ID to trade
252+
size: Number of contracts to trade
253+
account_id: Account ID. Uses default account if None.
254+
255+
Returns:
256+
OrderPlaceResponse: Response containing order ID and status
257+
258+
Example:
259+
>>> # Join the ask to provide liquidity
260+
>>> response = await order_manager.place_join_ask_order("MGC", 1)
261+
"""
262+
return await self.place_order(
263+
contract_id=contract_id,
264+
side=OrderSide.SELL,
265+
size=size,
266+
order_type=OrderType.JOIN_ASK,
267+
account_id=account_id,
268+
)

tests/order_manager/test_order_types.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,31 @@ async def test_place_trailing_stop_order(self):
6161
args = dummy.place_order.call_args.kwargs
6262
assert args["order_type"] == 5
6363
assert args["trail_price"] == 5.0
64+
65+
async def test_place_join_bid_order(self):
66+
"""place_join_bid_order delegates to place_order with order_type=6 and side=0 (buy)."""
67+
dummy = DummyOrderManager()
68+
from project_x_py.order_manager.order_types import OrderTypesMixin
69+
70+
mixin = OrderTypesMixin()
71+
mixin.place_order = dummy.place_order
72+
await mixin.place_join_bid_order("MGC", 2)
73+
args = dummy.place_order.call_args.kwargs
74+
assert args["order_type"] == 6
75+
assert args["side"] == 0 # Buy side
76+
assert args["size"] == 2
77+
assert args["contract_id"] == "MGC"
78+
79+
async def test_place_join_ask_order(self):
80+
"""place_join_ask_order delegates to place_order with order_type=7 and side=1 (sell)."""
81+
dummy = DummyOrderManager()
82+
from project_x_py.order_manager.order_types import OrderTypesMixin
83+
84+
mixin = OrderTypesMixin()
85+
mixin.place_order = dummy.place_order
86+
await mixin.place_join_ask_order("MGC", 3)
87+
args = dummy.place_order.call_args.kwargs
88+
assert args["order_type"] == 7
89+
assert args["side"] == 1 # Sell side
90+
assert args["size"] == 3
91+
assert args["contract_id"] == "MGC"

0 commit comments

Comments
 (0)