diff --git a/CHANGELOG.md b/CHANGELOG.md index bab4d7d..b635bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 96a5879..6264801 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 --------------------------------------------------- diff --git a/examples/16_join_orders.py b/examples/16_join_orders.py new file mode 100644 index 0000000..51b6511 --- /dev/null +++ b/examples/16_join_orders.py @@ -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()) diff --git a/pyproject.toml b/pyproject.toml index c85ebfd..183fbb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/src/project_x_py/__init__.py b/src/project_x_py/__init__.py index eb0662f..0863cf3 100644 --- a/src/project_x_py/__init__.py +++ b/src/project_x_py/__init__.py @@ -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 diff --git a/src/project_x_py/indicators/__init__.py b/src/project_x_py/indicators/__init__.py index d6de9e0..8b6efac 100644 --- a/src/project_x_py/indicators/__init__.py +++ b/src/project_x_py/indicators/__init__.py @@ -190,7 +190,7 @@ ) # Version info -__version__ = "2.0.6" +__version__ = "2.0.7" __author__ = "TexasCoding" diff --git a/src/project_x_py/order_manager/core.py b/src/project_x_py/order_manager/core.py index 0d21fea..ed71cbf 100644 --- a/src/project_x_py/order_manager/core.py +++ b/src/project_x_py/order_manager/core.py @@ -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 diff --git a/src/project_x_py/order_manager/order_types.py b/src/project_x_py/order_manager/order_types.py index a2b1cc4..639704f 100644 --- a/src/project_x_py/order_manager/order_types.py +++ b/src/project_x_py/order_manager/order_types.py @@ -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. @@ -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: @@ -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 @@ -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, + ) diff --git a/tests/order_manager/test_order_types.py b/tests/order_manager/test_order_types.py index fc0d9c1..09a2d23 100644 --- a/tests/order_manager/test_order_types.py +++ b/tests/order_manager/test_order_types.py @@ -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"