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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Old implementations are removed when improved
- Clean, modern code architecture is prioritized

## [2.0.7] - 2025-08-03

### Added
- **📈 JoinBid and JoinAsk Order Types**: Passive liquidity-providing order types
- `place_join_bid_order()`: Places limit buy order at current best bid price
- `place_join_ask_order()`: Places limit sell order at current best ask price
- These order types automatically join the best bid/ask queue
- Useful for market making strategies and minimizing market impact
- Added comprehensive tests for both order types
- Created example script `16_join_orders.py` demonstrating usage

### Improved
- **📖 Order Type Documentation**: Enhanced documentation for all order types
- Clarified that JoinBid/JoinAsk are passive orders, not stop-limit orders
- Updated order type enum documentation with behavior descriptions
- Added inline comments explaining each order type value

## [2.0.6] - 2025-08-03

### Changed
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
project = "project-x-py"
copyright = "2025, Jeff West"
author = "Jeff West"
release = "2.0.6"
version = "2.0.6"
release = "2.0.7"
version = "2.0.7"

# -- General configuration ---------------------------------------------------

Expand Down
129 changes: 129 additions & 0 deletions examples/16_join_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""
Example demonstrating JoinBid and JoinAsk order types.

JoinBid and JoinAsk orders are passive liquidity-providing orders that automatically
place limit orders at the current best bid or ask price. They're useful for:
- Market making strategies
- Providing liquidity
- Minimizing market impact
- Getting favorable queue position
"""

import asyncio
import os
import sys
from pathlib import Path

# Add src to Python path for development
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from project_x_py import ProjectX, create_order_manager


async def main():
"""Demonstrate JoinBid and JoinAsk order placement."""
# Initialize client
async with ProjectX.from_env() as client:
await client.authenticate()

# Create order manager
order_manager = create_order_manager(client)

# Contract to trade
contract = "MNQ"

print(f"=== JoinBid and JoinAsk Order Example for {contract} ===\n")

# Get current market data to show context
bars = await client.get_bars(contract, days=1, timeframe="1min")
if bars and not bars.is_empty():
latest = bars.tail(1)
print(f"Current market context:")
print(f" Last price: ${latest['close'][0]:,.2f}")
print(f" High: ${latest['high'][0]:,.2f}")
print(f" Low: ${latest['low'][0]:,.2f}\n")

try:
# Example 1: Place a JoinBid order
print("1. Placing JoinBid order (buy at best bid)...")
join_bid_response = await order_manager.place_join_bid_order(
contract_id=contract, size=1
)

if join_bid_response.success:
print(f"✅ JoinBid order placed successfully!")
print(f" Order ID: {join_bid_response.orderId}")
print(f" This order will buy at the current best bid price\n")
else:
print(f"❌ JoinBid order failed: {join_bid_response.message}\n")

# Wait a moment
await asyncio.sleep(2)

# Example 2: Place a JoinAsk order
print("2. Placing JoinAsk order (sell at best ask)...")
join_ask_response = await order_manager.place_join_ask_order(
contract_id=contract, size=1
)

if join_ask_response.success:
print(f"✅ JoinAsk order placed successfully!")
print(f" Order ID: {join_ask_response.orderId}")
print(f" This order will sell at the current best ask price\n")
else:
print(f"❌ JoinAsk order failed: {join_ask_response.message}\n")

# Show order status
print("3. Checking order status...")
active_orders = await order_manager.get_active_orders()

print(f"\nActive orders: {len(active_orders)}")
for order in active_orders:
if order.id in [join_bid_response.orderId, join_ask_response.orderId]:
order_type = "JoinBid" if order.side == 0 else "JoinAsk"
side = "Buy" if order.side == 0 else "Sell"
print(
f" - {order_type} Order {order.id}: {side} {order.size} @ ${order.price:,.2f}"
)

# Cancel orders to clean up
print("\n4. Cancelling orders...")
if join_bid_response.success:
cancel_result = await order_manager.cancel_order(
join_bid_response.orderId
)
if cancel_result.success:
print(f"✅ JoinBid order {join_bid_response.orderId} cancelled")

if join_ask_response.success:
cancel_result = await order_manager.cancel_order(
join_ask_response.orderId
)
if cancel_result.success:
print(f"✅ JoinAsk order {join_ask_response.orderId} cancelled")

except Exception as e:
print(f"❌ Error: {e}")

print("\n=== JoinBid/JoinAsk Example Complete ===")
print("\nKey Points:")
print("- JoinBid places a limit buy order at the current best bid")
print("- JoinAsk places a limit sell order at the current best ask")
print("- These are passive orders that provide liquidity")
print("- The actual fill price depends on market conditions")
print("- Useful for market making and minimizing market impact")


if __name__ == "__main__":
# Check for required environment variables
if not os.getenv("PROJECT_X_API_KEY") or not os.getenv("PROJECT_X_USERNAME"):
print(
"❌ Error: Please set PROJECT_X_API_KEY and PROJECT_X_USERNAME environment variables"
)
print("Example:")
print(' export PROJECT_X_API_KEY="your-api-key"')
print(' export PROJECT_X_USERNAME="your-username"')
sys.exit(1)

asyncio.run(main())
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "project-x-py"
version = "2.0.6"
version = "2.0.7"
description = "High-performance Python SDK for futures trading with real-time WebSocket data, technical indicators, order management, and market depth analysis"
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion src/project_x_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

from project_x_py.client.base import ProjectXBase

__version__ = "2.0.6"
__version__ = "2.0.7"
__author__ = "TexasCoding"

# Core client classes - renamed from Async* to standard names
Expand Down
2 changes: 1 addition & 1 deletion src/project_x_py/indicators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@
)

# Version info
__version__ = "2.0.6"
__version__ = "2.0.7"
__author__ = "TexasCoding"


Expand Down
5 changes: 3 additions & 2 deletions src/project_x_py/order_manager/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ class OrderManager(
Order Type Enum Values:
- 1: Limit
- 2: Market
- 3: StopLimit
- 4: Stop
- 5: TrailingStop
- 6: JoinBid
- 7: JoinAsk
- 6: JoinBid (places limit buy at current best bid)
- 7: JoinAsk (places limit sell at current best ask)

The OrderManager combines multiple mixins to provide a unified interface for all
order-related operations, ensuring consistent behavior and comprehensive functionality
Expand Down
74 changes: 73 additions & 1 deletion src/project_x_py/order_manager/order_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- Limit Orders: Execution at specified price or better
- Stop Orders: Market orders triggered at stop price
- Trailing Stop Orders: Dynamic stops that follow price movement
- Join Bid Orders: Limit buy orders at current best bid price
- Join Ask Orders: Limit sell orders at current best ask price

Each order type method provides a simplified interface for common order placement
scenarios while maintaining full compatibility with the underlying order system.
Expand All @@ -33,6 +35,8 @@
await om.place_market_order("MGC", 0, 1)
await om.place_stop_order("MGC", 1, 1, 2040.0)
await om.place_trailing_stop_order("MGC", 1, 1, 5.0)
await om.place_join_bid_order("MGC", 1) # Join bid side
await om.place_join_ask_order("MGC", 1) # Join ask side
```

See Also:
Expand All @@ -45,7 +49,7 @@
from typing import TYPE_CHECKING

from project_x_py.models import OrderPlaceResponse
from project_x_py.types.trading import OrderType
from project_x_py.types.trading import OrderSide, OrderType

if TYPE_CHECKING:
from project_x_py.types import OrderManagerProtocol
Expand Down Expand Up @@ -194,3 +198,71 @@ async def place_trailing_stop_order(
trail_price=trail_price,
account_id=account_id,
)

async def place_join_bid_order(
self: "OrderManagerProtocol",
contract_id: str,
size: int,
account_id: int | None = None,
) -> OrderPlaceResponse:
"""
Place a join bid order (limit order at current best bid price).

Join bid orders automatically place a limit buy order at the current
best bid price, joining the queue of passive liquidity providers.
The order will be placed at whatever the best bid price is at the
time of submission.

Args:
contract_id: The contract ID to trade
size: Number of contracts to trade
account_id: Account ID. Uses default account if None.

Returns:
OrderPlaceResponse: Response containing order ID and status

Example:
>>> # Join the bid to provide liquidity
>>> response = await order_manager.place_join_bid_order("MGC", 1)
"""
return await self.place_order(
contract_id=contract_id,
side=OrderSide.BUY,
size=size,
order_type=OrderType.JOIN_BID,
account_id=account_id,
)

async def place_join_ask_order(
self: "OrderManagerProtocol",
contract_id: str,
size: int,
account_id: int | None = None,
) -> OrderPlaceResponse:
"""
Place a join ask order (limit order at current best ask price).

Join ask orders automatically place a limit sell order at the current
best ask price, joining the queue of passive liquidity providers.
The order will be placed at whatever the best ask price is at the
time of submission.

Args:
contract_id: The contract ID to trade
size: Number of contracts to trade
account_id: Account ID. Uses default account if None.

Returns:
OrderPlaceResponse: Response containing order ID and status

Example:
>>> # Join the ask to provide liquidity
>>> response = await order_manager.place_join_ask_order("MGC", 1)
"""
return await self.place_order(
contract_id=contract_id,
side=OrderSide.SELL,
size=size,
order_type=OrderType.JOIN_ASK,
account_id=account_id,
)
28 changes: 28 additions & 0 deletions tests/order_manager/test_order_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,31 @@ async def test_place_trailing_stop_order(self):
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 5
assert args["trail_price"] == 5.0

async def test_place_join_bid_order(self):
"""place_join_bid_order delegates to place_order with order_type=6 and side=0 (buy)."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin

mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_join_bid_order("MGC", 2)
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 6
assert args["side"] == 0 # Buy side
assert args["size"] == 2
assert args["contract_id"] == "MGC"

async def test_place_join_ask_order(self):
"""place_join_ask_order delegates to place_order with order_type=7 and side=1 (sell)."""
dummy = DummyOrderManager()
from project_x_py.order_manager.order_types import OrderTypesMixin

mixin = OrderTypesMixin()
mixin.place_order = dummy.place_order
await mixin.place_join_ask_order("MGC", 3)
args = dummy.place_order.call_args.kwargs
assert args["order_type"] == 7
assert args["side"] == 1 # Sell side
assert args["size"] == 3
assert args["contract_id"] == "MGC"
Loading