diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4eebf..a06c1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,33 @@ 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 +## [3.0.2] - 2025-08-08 + +### Fixed +- **๐Ÿ› Order Lifecycle Tracking**: Fixed critical issues in order lifecycle tracking example + - Corrected asyncio.wait() usage by creating tasks instead of passing coroutines + - Fixed instrument lookup - recognized that suite.instrument is already an Instrument object + - Fixed Order field references (use `type` not `orderType`) + - Fixed Position field references (use `size` not `netQuantity`) + - Fixed cancel_order return type handling (returns bool not object) + +- **๐Ÿ”ง Order Templates**: Fixed instrument lookup issues + - Removed unnecessary async calls to get_instrument() + - suite.instrument is already resolved after TradingSuite initialization + +### Added +- **๐Ÿงน Cleanup Functionality**: Comprehensive cleanup for demos and examples + - Automatic cancellation of all open orders at demo completion + - Automatic closing of all open positions + - Cleanup runs in finally block to ensure execution even on errors + - Prevents accumulation of test orders when running examples repeatedly + +### Improved +- **๐Ÿ“š Documentation**: Updated all documentation to reflect v3.0.2 + - Updated version references throughout + - Added clear documentation of breaking changes + - Improved migration guide clarity + ## [3.0.1] - 2025-08-08 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 6ad6456..4c55e14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Status: v3.0.1 - Production-Ready Async Architecture +## Project Status: v3.0.2 - Production-Ready Async Architecture **IMPORTANT**: This project uses a fully asynchronous architecture. All APIs are async-only, optimized for high-performance futures trading. @@ -74,7 +74,7 @@ uv run python -m build # Alternative build command ## Project Architecture -### Core Components (v3.0.1 - Multi-file Packages) +### Core Components (v3.0.2 - Multi-file Packages) **ProjectX Client (`src/project_x_py/client/`)** - Main async API client for TopStepX ProjectX Gateway @@ -278,6 +278,12 @@ async with ProjectX.from_env() as client: ## Recent Changes +### v3.0.2 - Bug Fixes and Improvements +- **Order Lifecycle Tracking**: Fixed asyncio concurrency and field reference issues +- **Order Templates**: Fixed instrument lookup to use cached object +- **Cleanup Functionality**: Added comprehensive order/position cleanup +- **Documentation**: Updated all docs to reflect current version + ### v3.0.1 - Production Ready - **Performance Optimizations**: Enhanced connection pooling and caching - **Event Bus System**: Unified event handling across all components diff --git a/README.md b/README.md index a725b66..289cffe 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,17 @@ A **high-performance async Python SDK** for the [ProjectX Trading Platform](http This Python SDK acts as a bridge between your trading strategies and the ProjectX platform, handling all the complex API interactions, data processing, and real-time connectivity. -## ๐Ÿš€ v3.0.1 - TradingSuite Architecture +## ๐Ÿš€ v3.0.2 - Production-Ready Trading Suite -**Latest Update (v3.0.1)**: Complete architectural upgrade with TradingSuite for simplified SDK usage, feature flags, and unified event handling. +**Latest Update (v3.0.2)**: Bug fixes and improvements to order lifecycle tracking, comprehensive cleanup functionality, and enhanced error handling. -### What's New in v3.0.1 +### What's New in v3.0.2 + +- **Fixed Order Lifecycle Tracking**: Corrected asyncio concurrency issues and field references +- **Automatic Cleanup**: Added comprehensive cleanup for orders and positions in examples +- **Bug Fixes**: Fixed instrument lookup in order templates and improved error handling + +### Key Features from v3.0.1 - **TradingSuite Class**: New unified entry point for simplified SDK usage - **One-line Initialization**: TradingSuite.create() handles all setup @@ -33,7 +39,7 @@ This Python SDK acts as a bridge between your trading strategies and the Project - **Context Manager Support**: Automatic cleanup with async with statements - **Unified Event Handling**: Built-in EventBus for all components -**BREAKING CHANGE**: Version 3.0.1 replaces factory functions with TradingSuite. See migration guide below. +**BREAKING CHANGE**: Version 3.0+ replaces factory functions with TradingSuite. See migration guide below. ### Why Async? @@ -43,15 +49,15 @@ This Python SDK acts as a bridge between your trading strategies and the Project - **WebSocket Native**: Perfect for real-time trading applications - **Modern Python**: Leverages Python 3.12+ async features -### Migration to v3.0.1 +### Migration to v3.0+ -If you're upgrading from v2.x or v3.0.0, key changes include TradingSuite replacing factories: +If you're upgrading from v2.x, key changes include TradingSuite replacing factories: ```python -# Old (v2.x/v3.0.0) +# Old (v2.x) suite = await create_initialized_trading_suite(\"MNQ\", client) -# New (v3.0.1) +# New (v3.0+) suite = await TradingSuite.create(\"MNQ\") ``` @@ -120,7 +126,7 @@ if __name__ == \"__main__\": asyncio.run(main()) ``` -### Trading Suite (NEW in v3.0.1) +### Trading Suite (NEW in v3.0+) The easiest way to get started with a complete trading setup: diff --git a/SECURITY.md b/SECURITY.md index 02f4e86..9d4bd72 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,10 +10,11 @@ We currently provide security updates for the following versions: | Version | Supported | | ------- | ------------------ | -| 2.0.x | :white_check_mark: | +| 3.0.x | :white_check_mark: | +| 2.0.x | :x: | | 1.x.x | :x: | -Note: Version 2.0.0 was a complete rewrite with an async-only architecture, and all previous synchronous APIs were removed. +Note: Version 3.0.0 introduced the TradingSuite architecture, replacing all factory functions. Version 2.0.0 was a complete rewrite with an async-only architecture. ## Reporting a Vulnerability diff --git a/docs/conf.py b/docs/conf.py index 1ccd060..92837e4 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 = "3.0.1" -version = "3.0.1" +release = "3.0.2" +version = "3.0.2" # -- General configuration --------------------------------------------------- diff --git a/examples/03_position_management.py b/examples/03_position_management.py index 7c15650..84f8eb2 100644 --- a/examples/03_position_management.py +++ b/examples/03_position_management.py @@ -104,16 +104,14 @@ async def display_positions( try: # Get current market price current_price = await suite.data.get_current_price() - if current_price: - # Get instrument info for tick value - instrument_info = await suite.client.get_instrument( - suite.instrument - ) - tick_value = instrument_info.tickValue + if current_price and suite.instrument: + # Use the instrument already loaded in suite + instrument_info = suite.instrument + point_value = instrument_info.tickValue / instrument_info.tickSize # Calculate P&L using position manager's method pnl_data = await position_manager.calculate_position_pnl( - position, float(current_price), point_value=tick_value + position, float(current_price), point_value=point_value ) unrealized_pnl = pnl_data["unrealized_pnl"] except Exception: @@ -193,18 +191,19 @@ async def monitor_positions( # Try to calculate real P&L with current prices if suite and positions: current_price = await suite.data.get_current_price() - if current_price: - instrument_info = await suite.client.get_instrument( - suite.instrument + if current_price and suite.instrument: + # Use the instrument already loaded in suite + instrument_info = suite.instrument + point_value = ( + instrument_info.tickValue / instrument_info.tickSize ) - tick_value = instrument_info.tickValue for position in positions: pnl_data = ( await position_manager.calculate_position_pnl( position, float(current_price), - point_value=tick_value, + point_value=point_value, ) ) total_pnl += pnl_data["unrealized_pnl"] diff --git a/examples/04_realtime_data.py b/examples/04_realtime_data.py index d48f4f3..45d985e 100644 --- a/examples/04_realtime_data.py +++ b/examples/04_realtime_data.py @@ -34,7 +34,6 @@ TradingSuite, setup_logging, ) -from project_x_py.types.protocols import RealtimeDataManagerProtocol if TYPE_CHECKING: from project_x_py.realtime_data_manager import RealtimeDataManager @@ -46,16 +45,18 @@ async def display_current_prices(data_manager: "RealtimeDataManager") -> None: current_price = await data_manager.get_current_price() if current_price: - print(f" Current Price: ${current_price:.2f}") + print(f" Live Price: ${current_price:.2f}") else: - print(" Current Price: Not available") + print(" Live Price: Not available") - # Get multi-timeframe data asynchronously - get 1 bar from each timeframe + # Get multi-timeframe data asynchronously - get latest bars timeframes = ["15sec", "1min", "5min", "15min", "1hr"] mtf_tasks: list[Coroutine[Any, Any, pl.DataFrame | None]] = [] for tf in timeframes: - mtf_tasks.append(data_manager.get_data(tf, bars=1)) + mtf_tasks.append( + data_manager.get_data(tf) + ) # Get all data, we'll take the last bar # Get data from all timeframes concurrently mtf_results = await asyncio.gather(*mtf_tasks, return_exceptions=True) @@ -70,19 +71,26 @@ async def display_current_prices(data_manager: "RealtimeDataManager") -> None: continue if data.is_empty(): - print(f" {timeframe:>6}: No data") + print(f" {timeframe:>6}: Building bars...") continue + # Get the last complete bar latest_bar = data.tail(1) for row in latest_bar.iter_rows(named=True): timestamp = row["timestamp"] close = row["close"] volume = row["volume"] + # Format timestamp more concisely + time_str = ( + timestamp.strftime("%H:%M:%S") + if hasattr(timestamp, "strftime") + else str(timestamp) + ) print( - f" {timeframe:>6}: ${close:8.2f} @ {timestamp} (Vol: {volume:,})" + f" {timeframe:>6}: ${close:8.2f} @ {time_str} (Vol: {volume:,})" ) else: - print(f" {timeframe:>6}: No data") + print(f" {timeframe:>6}: Initializing...") async def display_memory_stats(data_manager: "RealtimeDataManager") -> None: @@ -91,17 +99,28 @@ async def display_memory_stats(data_manager: "RealtimeDataManager") -> None: # get_memory_stats is synchronous in async data manager stats = data_manager.get_memory_stats() print("\n๐Ÿ’พ Memory Statistics:") - print(f" Total Bars: {stats.get('total_bars', 0):,}") + print(f" Total Bars Stored: {stats.get('total_bars_stored', 0):,}") print(f" Ticks Processed: {stats.get('ticks_processed', 0):,}") - print(f" Bars Cleaned: {stats.get('bars_cleaned', 0):,}") - print(f" Tick Buffer Size: {stats.get('tick_buffer_size', 0):,}") - - # Show per-timeframe breakdown - breakdown = stats.get("timeframe_breakdown", {}) - if breakdown: - print(" Timeframe Breakdown:") - for tf, count in breakdown.items(): - print(f" {tf}: {count:,} bars") + print(f" Quotes Processed: {stats.get('quotes_processed', 0):,}") + print(f" Trades Processed: {stats.get('trades_processed', 0):,}") + + # Show per-timeframe breakdown (Note: this key doesn't exist in current implementation) + # Will need to calculate it manually from the data + print(" Bars per Timeframe:") + for tf in data_manager.timeframes: + if tf in data_manager.data: + count = len(data_manager.data[tf]) + if count > 0: + print(f" {tf}: {count:,} bars") + + # Also show validation status for more insight + validation = data_manager.get_realtime_validation_status() + if validation.get("is_running"): + print( + f" Feed Active: โœ… (Processing {validation.get('instrument', 'N/A')})" + ) + else: + print(" Feed Active: โŒ") except Exception as e: print(f" โŒ Memory stats error: {e}") @@ -113,11 +132,11 @@ async def display_system_statistics(data_manager: "RealtimeDataManager") -> None # Use validation status instead of get_statistics (which doesn't exist) stats = data_manager.get_realtime_validation_status() print("\n๐Ÿ“ˆ System Status:") - print(f" Instrument: {getattr(data_manager, 'instrument', 'Unknown')}") - print(f" Contract ID: {getattr(data_manager, 'contract_id', 'Unknown')}") - print(f" Real-time Enabled: {stats.get('realtime_enabled', False)}") - print(f" Connection Valid: {stats.get('connection_valid', False)}") - print(f" Data Valid: {stats.get('data_valid', False)}") + print(f" Instrument: {stats.get('instrument', 'Unknown')}") + print(f" Contract ID: {stats.get('contract_id', 'Unknown')}") + print(f" Real-time Feed Active: {stats.get('is_running', False)}") + print(f" Ticks Processed: {stats.get('ticks_processed', 0):,}") + print(f" Bars Cleaned: {stats.get('bars_cleaned', 0):,}") # Show data status per timeframe print(" Timeframe Status:") @@ -173,14 +192,17 @@ async def demonstrate_historical_analysis(data_manager: "RealtimeDataManager") - print(f" โŒ Analysis error: {e}") -async def new_bar_callback(data: dict[str, Any]) -> None: +async def new_bar_callback(event: Any) -> None: """Handle new bar creation asynchronously.""" timestamp = datetime.now().strftime("%H:%M:%S") - timeframe = data["timeframe"] - bar = data["data"] - print( - f"๐Ÿ“Š [{timestamp}] New {timeframe} Bar: ${bar['close']:.2f} (Vol: {bar['volume']:,})" - ) + # Extract data from the Event object + data = event.data if hasattr(event, "data") else event + timeframe = data.get("timeframe", "unknown") + bar = data.get("data", {}) + if bar and "close" in bar and "volume" in bar: + print( + f"๐Ÿ“Š [{timestamp}] New {timeframe} Bar: ${bar['close']:.2f} (Vol: {bar['volume']:,})" + ) async def main() -> bool: @@ -221,7 +243,6 @@ async def main() -> bool: # Components are now accessed as attributes data_manager = suite.data - realtime_client = suite.realtime print("\nโœ… All components connected and subscribed:") print(" - Real-time client connected") diff --git a/examples/05_orderbook_analysis.py b/examples/05_orderbook_analysis.py index 4ccf71d..4e30036 100644 --- a/examples/05_orderbook_analysis.py +++ b/examples/05_orderbook_analysis.py @@ -192,25 +192,39 @@ async def display_market_microstructure(orderbook: OrderBook) -> None: async def display_iceberg_detection(orderbook: OrderBook) -> None: """Display potential iceberg orders.""" # Use detect_iceberg_orders instead of detect_icebergs - icebergs = await orderbook.detect_iceberg_orders() + icebergs = await orderbook.detect_iceberg_orders( + min_refreshes=5, + volume_threshold=50, + time_window_minutes=10, + ) print("\n๐ŸงŠ Iceberg Detection:", flush=True) if icebergs and "iceberg_levels" in icebergs: iceberg_list = icebergs["iceberg_levels"] - for iceberg in iceberg_list[:3]: # Show top 3 - side = "BID" if iceberg["side"] == "bid" else "ASK" - print( - f" Potential {side} iceberg at ${iceberg['price']:,.2f}", - flush=True, - ) - print(f" - Visible: {iceberg.get('visible_size', 'N/A')}", flush=True) - print( - f" - Refill Count: {iceberg.get('refill_count', 'N/A')}", flush=True - ) - print( - f" - Total Volume: {iceberg.get('total_volume', 'N/A')}", flush=True - ) - print(f" - Confidence: {iceberg.get('confidence', 0):.1%}", flush=True) + if iceberg_list: + for iceberg in iceberg_list[:3]: # Show top 3 + side = "BID" if iceberg["side"] == "bid" else "ASK" + print( + f" Potential {side} iceberg at ${iceberg['price']:,.2f}", + flush=True, + ) + print( + f" - Avg Volume: {iceberg.get('avg_volume', 'N/A'):.1f}", + flush=True, + ) + print( + f" - Refresh Count: {iceberg.get('refresh_count', 'N/A')}", + flush=True, + ) + print( + f" - Replenishments: {iceberg.get('replenishment_count', 'N/A')}", + flush=True, + ) + print( + f" - Confidence: {iceberg.get('confidence', 0):.1%}", flush=True + ) + else: + print(" No iceberg orders detected", flush=True) else: print(" No iceberg orders detected", flush=True) @@ -295,18 +309,24 @@ async def demonstrate_comprehensive_methods(orderbook: OrderBook) -> None: try: order_stats = await orderbook.get_order_type_statistics() if order_stats: - print( - f" Market Orders: {order_stats.get('market_orders', 0):,}", flush=True + # The stats are tracked by DomType numbers + total_events = sum( + v for k, v in order_stats.items() if k.startswith("type_") ) - print( - f" Limit Orders: {order_stats.get('limit_orders', 0):,}", flush=True - ) - print(f" Stop Orders: {order_stats.get('stop_orders', 0):,}", flush=True) - if order_stats.get("avg_order_size"): - print( - f" Avg Order Size: {order_stats['avg_order_size']:.1f}", - flush=True, - ) + if total_events > 0: + print(f" Total Events: {total_events:,}", flush=True) + # Show top event types + top_types = sorted( + [(k, v) for k, v in order_stats.items() if k.startswith("type_")], + key=lambda x: x[1], + reverse=True, + )[:5] + for type_key, count in top_types: + if count > 0: + type_num = type_key.replace("type_", "").replace("_count", "") + print(f" Type {type_num}: {count:,} events", flush=True) + else: + print(" No order statistics available", flush=True) else: print(" No order statistics available", flush=True) except Exception as e: @@ -412,19 +432,6 @@ async def main() -> bool: print(" - 30 seconds: Comprehensive method demonstrations", flush=True) print("\nPress Ctrl+C at any time to stop early.", flush=True) - # Ask for confirmation - print("\nโ“ Continue with the full demonstration? (y/N): ", end="", flush=True) - try: - response = await asyncio.get_event_loop().run_in_executor( - None, lambda: input().strip().lower() - ) - if response != "y": - print("โŒ Orderbook analysis cancelled", flush=True) - return False - except (EOFError, KeyboardInterrupt): - print("\nโŒ Orderbook analysis cancelled", flush=True) - return False - try: # Initialize TradingSuite v3 with orderbook feature print("\n๐Ÿ”‘ Initializing TradingSuite v3 with orderbook...", flush=True) @@ -492,11 +499,14 @@ async def main() -> bool: # Memory stats memory_stats = await orderbook.get_memory_stats() print("\n๐Ÿ’พ Memory Usage:", flush=True) - print(f" Bid Entries: {memory_stats.get('bid_entries', 0):,}", flush=True) - print(f" Ask Entries: {memory_stats.get('ask_entries', 0):,}", flush=True) - print(f" Trades Stored: {memory_stats.get('trades_stored', 0):,}", flush=True) + print(f" Bid Depth: {memory_stats.get('avg_bid_depth', 0):,}", flush=True) + print(f" Ask Depth: {memory_stats.get('avg_ask_depth', 0):,}", flush=True) + print( + f" Trades Processed: {memory_stats.get('trades_processed', 0):,}", + flush=True, + ) print( - f" Memory Cleaned: {memory_stats.get('memory_cleanups', 0)} times", + f" Total Volume: {memory_stats.get('total_volume', 0):,}", flush=True, ) diff --git a/examples/06_advanced_orderbook.py b/examples/06_advanced_orderbook.py new file mode 100755 index 0000000..aa582a1 --- /dev/null +++ b/examples/06_advanced_orderbook.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +""" +Advanced Orderbook Methods Example + +Demonstrates ALL advanced orderbook analysis methods including: +- Market microstructure analysis +- Iceberg order detection with proper thresholds +- Order clustering detection +- Volume profile analysis +- Support/resistance level identification +- Liquidity analysis with correct parameters +- Spread analysis over time +- Order flow imbalance +- Market impact estimation +- Comprehensive statistics + +This example shows how to properly use each method with appropriate +parameters for meaningful results. + +Author: TexasCoding +Date: 2025-08-08 +""" + +import asyncio +import sys +from datetime import datetime + +from project_x_py import TradingSuite, setup_logging + + +async def demonstrate_market_microstructure(orderbook): + """Demonstrate market microstructure analysis.""" + print("\n" + "=" * 60) + print("๐Ÿ”ฌ MARKET MICROSTRUCTURE ANALYSIS") + print("=" * 60) + + # Get advanced market metrics + metrics = await orderbook.get_advanced_market_metrics() + + if metrics: + # Book pressure analysis + if "book_pressure" in metrics: + bp = metrics["book_pressure"] + print("\n๐Ÿ“Š Book Pressure:") + print(f" Bid Pressure: {bp.get('bid_pressure', 0):.2f}") + print(f" Ask Pressure: {bp.get('ask_pressure', 0):.2f}") + print(f" Pressure Ratio: {bp.get('pressure_ratio', 0):.2%}") + if bp.get("pressure_ratio", 0) > 0.1: + print(" Signal: Buying pressure dominant ๐ŸŸข") + elif bp.get("pressure_ratio", 0) < -0.1: + print(" Signal: Selling pressure dominant ๐Ÿ”ด") + else: + print(" Signal: Balanced โš–๏ธ") + + # Trade intensity + if "trade_intensity" in metrics: + ti = metrics["trade_intensity"] + print(f"\nโšก Trade Intensity:") + print(f" Trades/Minute: {ti.get('trades_per_minute', 0):.1f}") + print(f" Volume/Minute: {ti.get('volume_per_minute', 0):,.0f}") + print(f" Avg Trade Size: {ti.get('avg_trade_size', 0):.1f}") + + # Price concentration + if "price_concentration" in metrics: + pc = metrics["price_concentration"] + print(f"\n๐ŸŽฏ Price Concentration:") + print(f" Bid Concentration: {pc.get('bid_concentration', 0):.3f}") + print(f" Ask Concentration: {pc.get('ask_concentration', 0):.3f}") + else: + print(" No microstructure data available") + + +async def demonstrate_iceberg_detection(orderbook): + """Demonstrate iceberg order detection with proper parameters.""" + print("\n" + "=" * 60) + print("๐ŸงŠ ICEBERG ORDER DETECTION") + print("=" * 60) + + # Use lower thresholds for better detection + icebergs = await orderbook.detect_iceberg_orders( + min_refreshes=2, # Lower threshold + volume_threshold=10, # Lower volume requirement + time_window_minutes=5, # Shorter window + ) + + if icebergs and "iceberg_levels" in icebergs: + levels = icebergs["iceberg_levels"] + if levels: + print(f"\nโœ… Detected {len(levels)} potential iceberg orders:") + for i, iceberg in enumerate(levels[:5], 1): + side = iceberg["side"].upper() + print(f"\n {i}. {side} Iceberg at ${iceberg['price']:,.2f}") + print(f" Average Volume: {iceberg.get('avg_volume', 0):.1f}") + print(f" Refresh Count: {iceberg.get('refresh_count', 0)}") + print(f" Replenishments: {iceberg.get('replenishment_count', 0)}") + print( + f" Hidden Size Est: {iceberg.get('estimated_hidden_size', 0):,.0f}" + ) + print(f" Confidence: {iceberg.get('confidence', 0):.1%}") + else: + print("\n No iceberg orders detected") + print(" Note: Icebergs require specific refresh patterns to detect") + + print("\n๐Ÿ’ก Detection Parameters Used:") + params = icebergs.get("detection_parameters", {}) + print(f" Min Refreshes: {params.get('min_refreshes', 'N/A')}") + print(f" Volume Threshold: {params.get('volume_threshold', 'N/A')}") + print(f" Time Window: {icebergs.get('analysis_window_minutes', 'N/A')} minutes") + + +async def demonstrate_order_clustering(orderbook): + """Demonstrate order clustering detection.""" + print("\n" + "=" * 60) + print("๐ŸŽฏ ORDER CLUSTERING ANALYSIS") + print("=" * 60) + + clusters = await orderbook.detect_order_clusters( + min_cluster_size=3, price_tolerance=0.25 + ) + + if clusters: + print(f"\nโœ… Found {len(clusters)} order clusters:") + for i, cluster in enumerate(clusters[:5], 1): + side = cluster.get("side", "unknown").upper() + print(f"\n {i}. {side} Cluster at ${cluster['center_price']:,.2f}") + print(f" Price Range: ${cluster.get('price_range', 0):.2f}") + print(f" Order Count: {cluster.get('cluster_size', 0)}") + print(f" Total Volume: {cluster.get('total_volume', 0):,}") + print(f" Avg Order Size: {cluster.get('avg_order_size', 0):.1f}") + + significance = cluster.get("significance", "") + if significance: + print(f" Significance: {significance}") + else: + print("\n No significant order clusters detected") + + +async def demonstrate_volume_profile(orderbook): + """Demonstrate volume profile analysis.""" + print("\n" + "=" * 60) + print("๐Ÿ“Š VOLUME PROFILE ANALYSIS") + print("=" * 60) + + profile = await orderbook.get_volume_profile(time_window_minutes=30, price_bins=10) + + if profile and profile.get("poc"): + print(f"\nโœ… Volume Profile (30-minute window):") + print(f" Point of Control (POC): ${profile['poc']:,.2f}") + print(f" Value Area High: ${profile.get('value_area_high', 0):,.2f}") + print(f" Value Area Low: ${profile.get('value_area_low', 0):,.2f}") + print(f" Total Volume: {profile.get('total_volume', 0):,}") + + # Show top volume levels + if "price_bins" in profile and "volumes" in profile: + bins = profile["price_bins"] + vols = profile["volumes"] + if bins and vols: + print("\n Top Volume Levels:") + sorted_levels = sorted( + zip(bins, vols), key=lambda x: x[1], reverse=True + ) + for price, vol in sorted_levels[:3]: + if vol > 0: + pct = ( + (vol / profile["total_volume"] * 100) + if profile["total_volume"] > 0 + else 0 + ) + print(f" ${price:,.2f}: {vol:,} ({pct:.1f}%)") + else: + print("\n Insufficient trade data for volume profile") + print(" Note: Volume profile requires trade history") + + +async def demonstrate_support_resistance(orderbook): + """Demonstrate support and resistance level detection.""" + print("\n" + "=" * 60) + print("๐Ÿ“ˆ SUPPORT & RESISTANCE LEVELS") + print("=" * 60) + + levels = await orderbook.get_support_resistance_levels( + lookback_minutes=60, min_touches=2, price_tolerance=0.25 + ) + + if levels: + current_price = (await orderbook.get_best_bid_ask()).get("mid", 0) + + # Support levels + supports = levels.get("support_levels", []) + if supports: + print(f"\n๐Ÿ“‰ Support Levels (below ${current_price:,.2f}):") + for i, level in enumerate(supports[:3], 1): + print(f" S{i}: ${level['price']:,.2f}") + print(f" Touches: {level.get('touches', 0)}") + print(f" Strength: {level.get('strength', 0):.1%}") + print(f" Volume: {level.get('volume_at_level', 0):,}") + + # Resistance levels + resistances = levels.get("resistance_levels", []) + if resistances: + print(f"\n๐Ÿ“ˆ Resistance Levels (above ${current_price:,.2f}):") + for i, level in enumerate(resistances[:3], 1): + print(f" R{i}: ${level['price']:,.2f}") + print(f" Touches: {level.get('touches', 0)}") + print(f" Strength: {level.get('strength', 0):.1%}") + print(f" Volume: {level.get('volume_at_level', 0):,}") + + # Key statistics + stats = levels.get("statistics", {}) + if stats: + print("\n๐Ÿ“Š Level Statistics:") + print(f" Strongest Support: ${stats.get('strongest_support', 0):,.2f}") + print( + f" Strongest Resistance: ${stats.get('strongest_resistance', 0):,.2f}" + ) + print(f" Price Range Analyzed: ${stats.get('price_range', 0):.2f}") + else: + print("\n No significant support/resistance levels detected") + + +async def demonstrate_liquidity_analysis(orderbook): + """Demonstrate liquidity analysis with proper parameters.""" + print("\n" + "=" * 60) + print("๐Ÿ’ง LIQUIDITY ANALYSIS") + print("=" * 60) + + # Get current orderbook state first + bids = await orderbook.get_orderbook_bids(levels=20) + asks = await orderbook.get_orderbook_asks(levels=20) + + # Show raw liquidity + if not bids.is_empty() and not asks.is_empty(): + bid_liquidity = bids["volume"].sum() + ask_liquidity = asks["volume"].sum() + + print(f"\n๐Ÿ“Š Current Liquidity (20 levels):") + print(f" Bid Liquidity: {bid_liquidity:,} contracts") + print(f" Ask Liquidity: {ask_liquidity:,} contracts") + print(f" Total Liquidity: {bid_liquidity + ask_liquidity:,} contracts") + + # Find significant levels (volume > average) + avg_bid_vol = bids["volume"].mean() if not bids.is_empty() else 0 + avg_ask_vol = asks["volume"].mean() if not asks.is_empty() else 0 + + print(f"\n๐ŸŽฏ Significant Levels (above average):") + print(f" Average Bid Size: {avg_bid_vol:.1f}") + print(f" Average Ask Size: {avg_ask_vol:.1f}") + + # Show large bid levels + large_bids = bids.filter(bids["volume"] > avg_bid_vol * 1.5) + if not large_bids.is_empty(): + print("\n Large Bid Levels:") + for row in large_bids.head(3).to_dicts(): + print(f" ${row['price']:,.2f}: {row['volume']} contracts") + + # Show large ask levels + large_asks = asks.filter(asks["volume"] > avg_ask_vol * 1.5) + if not large_asks.is_empty(): + print("\n Large Ask Levels:") + for row in large_asks.head(3).to_dicts(): + print(f" ${row['price']:,.2f}: {row['volume']} contracts") + + # Use get_liquidity_levels with appropriate threshold + liquidity = await orderbook.get_liquidity_levels( + min_volume=int(avg_bid_vol) if avg_bid_vol > 0 else 5, levels=20 + ) + + if liquidity: + sig_bids = liquidity.get("bid_levels", []) + sig_asks = liquidity.get("ask_levels", []) + + if sig_bids or sig_asks: + print(f"\n๐Ÿ’Ž Premium Liquidity Levels:") + print(f" Significant Bid Levels: {len(sig_bids)}") + print(f" Significant Ask Levels: {len(sig_asks)}") + + +async def demonstrate_spread_analysis(orderbook): + """Demonstrate spread analysis over time.""" + print("\n" + "=" * 60) + print("๐Ÿ“ SPREAD ANALYSIS") + print("=" * 60) + + spread_analysis = await orderbook.get_spread_analysis(window_minutes=15) + + if spread_analysis: + print(f"\nโœ… Spread Analysis (15-minute window):") + print(f" Current Spread: ${spread_analysis.get('current_spread', 0):.2f}") + print(f" Average Spread: ${spread_analysis.get('avg_spread', 0):.2f}") + print(f" Min Spread: ${spread_analysis.get('min_spread', 0):.2f}") + print(f" Max Spread: ${spread_analysis.get('max_spread', 0):.2f}") + print( + f" Spread Volatility: {spread_analysis.get('spread_volatility', 0):.3f}" + ) + + # Spread distribution + if "spread_distribution" in spread_analysis: + dist = spread_analysis["spread_distribution"] + print("\n Spread Distribution:") + for spread_val, pct in dist.items(): + print(f" ${spread_val}: {pct:.1f}%") + else: + print("\n Insufficient data for spread analysis") + + +async def demonstrate_cumulative_delta(orderbook): + """Demonstrate order flow imbalance analysis.""" + print("\n" + "=" * 60) + print("โš–๏ธ CUMULATIVE DELTA ANALYSIS") + print("=" * 60) + + # Use get_cumulative_delta with correct parameter name + delta = await orderbook.get_cumulative_delta(time_window_minutes=10) + + if delta: + print(f"\nโœ… Delta Analysis (10-minute window):") + print(f" Buy Volume: {delta.get('buy_volume', 0):,}") + print(f" Sell Volume: {delta.get('sell_volume', 0):,}") + print(f" Cumulative Delta: {delta.get('cumulative_delta', 0):+,}") + + # Calculate imbalance ratio + total_vol = delta.get("buy_volume", 0) + delta.get("sell_volume", 0) + if total_vol > 0: + imbalance_ratio = ( + delta.get("buy_volume", 0) - delta.get("sell_volume", 0) + ) / total_vol + print(f" Imbalance Ratio: {imbalance_ratio:+.2%}") + + # Flow direction + if imbalance_ratio > 0.2: + print(" Flow Direction: Strong Buying ๐ŸŸข๐ŸŸข") + elif imbalance_ratio > 0.1: + print(" Flow Direction: Buying ๐ŸŸข") + elif imbalance_ratio < -0.2: + print(" Flow Direction: Strong Selling ๐Ÿ”ด๐Ÿ”ด") + elif imbalance_ratio < -0.1: + print(" Flow Direction: Selling ๐Ÿ”ด") + else: + print(" Flow Direction: Balanced โš–๏ธ") + + # Delta trend + if "delta_trend" in delta: + print(f"\n Delta Trend: {delta['delta_trend']}") + else: + print("\n Insufficient trade data for delta analysis") + + +async def demonstrate_market_depth_impact(orderbook): + """Demonstrate market impact estimation.""" + print("\n" + "=" * 60) + print("๐Ÿ’ฅ MARKET DEPTH & IMPACT ANALYSIS") + print("=" * 60) + + # Use get_orderbook_depth for market impact analysis + price_ranges = [10.0, 25.0, 50.0, 100.0] + + for price_range in price_ranges: + print(f"\n๐Ÿ“ฆ Market Depth within {price_range:.0f} tick range:") + + # Get market depth analysis + depth_analysis = await orderbook.get_orderbook_depth(price_range=price_range) + + if depth_analysis: + print(f" Total Bid Volume: {depth_analysis.get('total_bid_volume', 0):,}") + print(f" Total Ask Volume: {depth_analysis.get('total_ask_volume', 0):,}") + print( + f" Estimated Fill Price: ${depth_analysis.get('estimated_fill_price', 0):,.2f}" + ) + print(f" Price Impact: {depth_analysis.get('price_impact_pct', 0):.2%}") + print(f" Levels Consumed: {depth_analysis.get('levels_consumed', 0)}") + print( + f" Market Depth Score: {depth_analysis.get('market_depth_score', 0):.2f}" + ) + else: + print(" Insufficient depth data") + + +async def demonstrate_comprehensive_stats(orderbook): + """Demonstrate comprehensive orderbook statistics.""" + print("\n" + "=" * 60) + print("๐Ÿ“ˆ COMPREHENSIVE STATISTICS") + print("=" * 60) + + stats = await orderbook.get_statistics() + + if stats: + print("\nโœ… Orderbook Statistics:") + + # Depth stats + print(f"\n๐Ÿ“Š Depth Statistics:") + print(f" Bid Levels: {stats.get('bid_depth', 0)}") + print(f" Ask Levels: {stats.get('ask_depth', 0)}") + print(f" Total Bid Volume: {stats.get('total_bid_size', 0):,}") + print(f" Total Ask Volume: {stats.get('total_ask_size', 0):,}") + + # Trade stats + print(f"\n๐Ÿ“‰ Trade Statistics:") + print(f" Total Trades: {stats.get('total_trades', 0):,}") + print(f" Buy Trades: {stats.get('buy_trades', 0):,}") + print(f" Sell Trades: {stats.get('sell_trades', 0):,}") + print(f" Avg Trade Size: {stats.get('avg_trade_size', 0):.1f}") + + # Price stats + print(f"\n๐Ÿ’ฐ Price Statistics:") + print(f" VWAP: ${stats.get('vwap', 0):,.2f}") + print(f" Current Mid: ${stats.get('mid_price', 0):,.2f}") + print(f" Session High: ${stats.get('session_high', 0):,.2f}") + print(f" Session Low: ${stats.get('session_low', 0):,.2f}") + + # Performance + memory = await orderbook.get_memory_stats() + print(f"\nโšก Performance:") + print(f" Updates Processed: {stats.get('level2_update_count', 0):,}") + print(f" Memory Cleanups: {memory.get('memory_cleanups', 0)}") + print(f" Total Volume: {memory.get('total_volume', 0):,}") + + +async def main(): + """Main demonstration of advanced orderbook methods.""" + logger = setup_logging(level="INFO") + + print("๐Ÿš€ Advanced Orderbook Methods Demonstration") + print("=" * 60) + print("\nThis example demonstrates ALL advanced orderbook analysis methods") + print("with proper parameters for meaningful results.\n") + + try: + # Initialize trading suite with orderbook + print("๐Ÿ”‘ Initializing TradingSuite with orderbook...") + suite = await TradingSuite.create( + "MNQ", features=["orderbook"], timeframes=["1min", "5min"], initial_days=1 + ) + + print("โœ… Suite initialized successfully!") + print(f" Account: {suite.client.account_info.name}") + print(f" Tracking: {suite.orderbook.instrument}") + + # Wait for initial data + print("\nโณ Collecting market data for 10 seconds...") + await asyncio.sleep(10) + + orderbook = suite.orderbook + + # Run all demonstrations + await demonstrate_market_microstructure(orderbook) + await demonstrate_iceberg_detection(orderbook) + await demonstrate_order_clustering(orderbook) + await demonstrate_volume_profile(orderbook) + await demonstrate_support_resistance(orderbook) + await demonstrate_liquidity_analysis(orderbook) + await demonstrate_spread_analysis(orderbook) + await demonstrate_cumulative_delta(orderbook) + await demonstrate_market_depth_impact(orderbook) + await demonstrate_comprehensive_stats(orderbook) + + # Final summary + print("\n" + "=" * 60) + print("โœ… DEMONSTRATION COMPLETE") + print("=" * 60) + print("\n๐Ÿ“ Key Takeaways:") + print(" โ€ข Use appropriate thresholds for each method") + print(" โ€ข Some patterns (icebergs) require specific market conditions") + print(" โ€ข Liquidity analysis needs proper volume thresholds") + print(" โ€ข Volume profile requires sufficient trade history") + print(" โ€ข Combine multiple methods for comprehensive analysis") + + await suite.disconnect() + return True + + except KeyboardInterrupt: + print("\nโน๏ธ Demonstration interrupted by user") + return False + except Exception as e: + logger.error(f"Demonstration failed: {e}") + print(f"\nโŒ Error: {e}") + return False + + +if __name__ == "__main__": + success = asyncio.run(main()) + sys.exit(0 if success else 1) diff --git a/examples/10_unified_event_system.py b/examples/10_unified_event_system.py index f415a02..f88c761 100644 --- a/examples/10_unified_event_system.py +++ b/examples/10_unified_event_system.py @@ -2,7 +2,7 @@ """ Example 10: Unified Event System with EventBus -This example demonstrates the new unified event system in ProjectX SDK v3.0.0. +This example demonstrates the new unified event system in ProjectX SDK v3.0.2. Instead of registering callbacks with individual components, all events flow through a single EventBus accessible via the TradingSuite. diff --git a/examples/11_simplified_data_access.py b/examples/11_simplified_data_access.py index 18a029a..cc40bc5 100644 --- a/examples/11_simplified_data_access.py +++ b/examples/11_simplified_data_access.py @@ -3,7 +3,7 @@ Example: Simplified Data Access with v3.0.0 This example demonstrates the new convenience methods for accessing market data -in the ProjectX SDK v3.0.0. These methods provide a cleaner, more intuitive API +in the ProjectX SDK v3.0.2. These methods provide a cleaner, more intuitive API for common data access patterns. Key improvements: @@ -15,7 +15,7 @@ - is_data_ready() - Check if enough data is loaded - get_bars_since() - Get data since a specific time -Author: SDK v3.0.0 Examples +Author: SDK v3.0.2 Examples """ import asyncio @@ -171,6 +171,6 @@ async def main() -> None: if __name__ == "__main__": - print("ProjectX SDK v3.0.0 - Simplified Data Access") + print("ProjectX SDK v3.0.2 - Simplified Data Access") print("=" * 50) asyncio.run(main()) diff --git a/examples/12_simplified_multi_timeframe.py b/examples/12_simplified_multi_timeframe.py index ca9241e..fe2fe54 100644 --- a/examples/12_simplified_multi_timeframe.py +++ b/examples/12_simplified_multi_timeframe.py @@ -8,7 +8,7 @@ Compare this with 06_multi_timeframe_strategy.py to see the improvements! -Author: SDK v3.0.0 Examples +Author: SDK v3.0.2 Examples """ import asyncio @@ -294,6 +294,6 @@ async def main() -> None: if __name__ == "__main__": - print("ProjectX SDK v3.0.0 - Simplified Multi-Timeframe Strategy") + print("ProjectX SDK v3.0.2 - Simplified Multi-Timeframe Strategy") print("=" * 60) asyncio.run(main()) diff --git a/examples/13_enhanced_models.py b/examples/13_enhanced_models.py index 0c12f18..bb97e02 100644 --- a/examples/13_enhanced_models.py +++ b/examples/13_enhanced_models.py @@ -9,7 +9,7 @@ - Position: is_long, is_short, direction, symbol, signed_size, unrealized_pnl() - Order: is_open, is_filled, is_buy, is_sell, side_str, type_str, status_str, filled_percent -Author: SDK v3.0.0 Examples +Author: SDK v3.0.2 Examples """ import asyncio @@ -261,6 +261,6 @@ async def main(): if __name__ == "__main__": - print("ProjectX SDK v3.0.0 - Enhanced Models Demo") + print("ProjectX SDK v3.0.2 - Enhanced Models Demo") print("=" * 50) asyncio.run(main()) diff --git a/examples/14_phase4_comprehensive_test.py b/examples/14_phase4_comprehensive_test.py index c785aa8..9ba7b95 100644 --- a/examples/14_phase4_comprehensive_test.py +++ b/examples/14_phase4_comprehensive_test.py @@ -7,7 +7,7 @@ - Enhanced model properties - Cleaner strategy implementation -Author: SDK v3.0.0 Testing +Author: SDK v3.0.2 Testing """ import asyncio @@ -326,7 +326,7 @@ async def demonstrate_phase4_improvements() -> None: async with await TradingSuite.create( "MNQ", timeframes=["1min", "5min", "15min"], initial_days=2 ) as suite: - print("ProjectX SDK v3.0.0 - Phase 4 Comprehensive Test") + print("ProjectX SDK v3.0.2 - Phase 4 Comprehensive Test") print("=" * 60) strategy = CleanTradingStrategy(suite) diff --git a/examples/15_order_lifecycle_tracking.py b/examples/15_order_lifecycle_tracking.py index 55fa2a4..d5110d5 100644 --- a/examples/15_order_lifecycle_tracking.py +++ b/examples/15_order_lifecycle_tracking.py @@ -13,7 +13,7 @@ - Order chain builder for complex orders - Common order templates -Author: SDK v3.0.0 Examples +Author: SDK v3.0.2 Examples """ import asyncio @@ -299,7 +299,11 @@ async def demonstrate_advanced_tracking() -> None: # Wait for any to fill print("Waiting for first fill...") - fill_tasks = [tracker.wait_for_fill(timeout=5) for tracker in trackers] + # Create tasks instead of coroutines + fill_tasks = [ + asyncio.create_task(tracker.wait_for_fill(timeout=5)) + for tracker in trackers + ] try: # Wait for first fill @@ -377,8 +381,100 @@ async def on_order_event(event: Any) -> None: await suite.off(EventType.ORDER_CANCELLED, on_order_event) +async def cleanup_demo_orders_and_positions() -> None: + """Clean up any open orders and positions created during the demo.""" + print("\n" + "=" * 50) + print("=== Demo Cleanup ===") + print("=" * 50 + "\n") + + async with await TradingSuite.create("MNQ") as suite: + print("Cleaning up demo orders and positions...\n") + + # 1. Cancel all open orders + print("1. Checking for open orders...") + open_orders = await suite.orders.search_open_orders() + + if open_orders: + print(f" Found {len(open_orders)} open orders to cancel:") + for order in open_orders: + try: + success = await suite.orders.cancel_order(order.id) + if success: + # Get order type and side names safely + order_type = ( + "LIMIT" + if order.type == 1 + else "MARKET" + if order.type == 2 + else "STOP" + if order.type == 4 + else str(order.type) + ) + side = ( + "BUY" + if order.side == 0 + else "SELL" + if order.side == 1 + else str(order.side) + ) + print(f" โœ… Cancelled order {order.id} ({order_type} {side})") + else: + print(f" โš ๏ธ Failed to cancel order {order.id}") + except Exception as e: + print(f" โš ๏ธ Error cancelling order {order.id}: {e}") + else: + print(" No open orders found") + + print() + + # 2. Close all open positions + print("2. Checking for open positions...") + positions = await suite.positions.get_all_positions() + + if positions: + print(f" Found {len(positions)} open positions to close:") + for position in positions: + if position.size != 0: + try: + # Place a market order to close the position + # Position type: 1=LONG, 2=SHORT + side = ( + 1 if position.type == 1 else 0 + ) # SELL if long, BUY if short + size = position.size # size is always positive + + result = await suite.orders.place_market_order( + contract_id=position.contractId, side=side, size=size + ) + + if result.success: + position_type = ( + "LONG" + if position.type == 1 + else "SHORT" + if position.type == 2 + else "UNKNOWN" + ) + print( + f" โœ… Closed {position_type} position in {position.contractId} (Size: {position.size})" + ) + else: + print( + f" โš ๏ธ Failed to close position in {position.contractId}: {result.errorMessage}" + ) + except Exception as e: + print( + f" โš ๏ธ Error closing position in {position.contractId}: {e}" + ) + else: + print(" No open positions found") + + print("\nโœ… Demo cleanup complete!") + + async def main() -> None: """Run all demonstrations.""" + suite = None try: # Basic order tracking await demonstrate_order_tracker() @@ -399,9 +495,15 @@ async def main() -> None: import traceback traceback.print_exc() + finally: + # Always run cleanup, even if demo fails + try: + await cleanup_demo_orders_and_positions() + except Exception as cleanup_error: + print(f"\nโš ๏ธ Cleanup error: {cleanup_error}") if __name__ == "__main__": - print("ProjectX SDK v3.0.0 - Order Lifecycle Tracking") + print("ProjectX SDK v3.0.2 - Order Lifecycle Tracking") print("=" * 50) asyncio.run(main()) diff --git a/examples/15_risk_management.py b/examples/15_risk_management.py index 27b304c..738c17c 100644 --- a/examples/15_risk_management.py +++ b/examples/15_risk_management.py @@ -2,7 +2,7 @@ """ Example 15: Risk Management with TradingSuite -Demonstrates the comprehensive risk management features of the SDK v3.0.0: +Demonstrates the comprehensive risk management features of the SDK v3.0.2: - Position sizing based on risk parameters - Trade validation against risk rules - Automatic stop-loss and take-profit attachment @@ -30,7 +30,7 @@ async def main() -> None: """Demonstrate risk management features.""" - print("=== ProjectX SDK v3.0.0 - Risk Management Example ===\n") + print("=== ProjectX SDK v3.0.2 - Risk Management Example ===\n") # Create trading suite with risk management enabled print("Creating TradingSuite with risk management...") diff --git a/examples/16_join_orders.py b/examples/16_join_orders.py index bb9f405..5f7b08a 100644 --- a/examples/16_join_orders.py +++ b/examples/16_join_orders.py @@ -134,19 +134,73 @@ async def main() -> None: # Cancel orders to clean up if order_ids: print("\n4. Cancelling orders...") - if join_bid_response and join_bid_response.success: - cancel_result = await suite.orders.cancel_order( + for order_id, order_type in [ + ( join_bid_response.orderId - ) - if cancel_result: - print(f"โœ… JoinBid order {join_bid_response.orderId} cancelled") - - if join_ask_response and join_ask_response.success: - cancel_result = await suite.orders.cancel_order( + if join_bid_response and join_bid_response.success + else None, + "JoinBid", + ), + ( join_ask_response.orderId - ) - if cancel_result: - print(f"โœ… JoinAsk order {join_ask_response.orderId} cancelled") + if join_ask_response and join_ask_response.success + else None, + "JoinAsk", + ), + ]: + if order_id: + try: + cancel_result = await suite.orders.cancel_order(order_id) + if cancel_result: + print(f"โœ… {order_type} order {order_id} cancelled") + except Exception as e: + # Order might have been filled or already cancelled + print( + f"โ„น๏ธ {order_type} order {order_id} could not be cancelled: {str(e).split(':')[-1].strip()}" + ) + print( + f" (Order may have been filled or already cancelled)" + ) + + # Check for any open positions that need to be closed + print("\n5. Checking for open positions...") + await asyncio.sleep(1) # Allow time for position updates + + positions = await suite.positions.get_all_positions() + if positions: + print(f"Found {len(positions)} open position(s)") + for position in positions: + if position.size != 0: + # Determine if position is long or short based on type + position_type = "LONG" if position.type == 1 else "SHORT" + print( + f" - {position.contractId}: {position_type} {position.size} contracts @ ${position.averagePrice:,.2f}" + ) + + # Close the position with a market order + # If LONG (type=1), we need to SELL (side=1) to close + # If SHORT (type=2), we need to BUY (side=0) to close + side = 1 if position.type == 1 else 0 + + print(f" Closing position with market order...") + try: + close_order = await suite.orders.place_market_order( + contract_id=position.contractId, + side=side, + size=position.size, + ) + if close_order and close_order.success: + print( + f" โœ… Position closed with order {close_order.orderId}" + ) + else: + print( + f" โš ๏ธ Failed to close position: {close_order.errorMessage if close_order else 'Unknown error'}" + ) + except Exception as e: + print(f" โš ๏ธ Error closing position: {e}") + else: + print("โœ… No open positions found") except Exception as e: print(f"โŒ Error: {e}") diff --git a/examples/16_managed_trades.py b/examples/16_managed_trades.py index 29503a5..283d55b 100644 --- a/examples/16_managed_trades.py +++ b/examples/16_managed_trades.py @@ -180,7 +180,7 @@ async def risk_validation_demo(suite: TradingSuite) -> None: async def main() -> None: """Run risk management examples.""" - print("=== ProjectX SDK v3.0.0 - Managed Trades Example ===\n") + print("=== ProjectX SDK v3.0.2 - Managed Trades Example ===\n") # Create trading suite with risk management print("Creating TradingSuite with risk management...") diff --git a/pyproject.toml b/pyproject.toml index 123780c..31c1e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "project-x-py" -version = "3.0.1" +version = "3.0.2" 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 2ec77a9..7cbe364 100644 --- a/src/project_x_py/__init__.py +++ b/src/project_x_py/__init__.py @@ -95,7 +95,7 @@ from project_x_py.client.base import ProjectXBase -__version__ = "3.0.1" +__version__ = "3.0.2" __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 7dc3989..73e283f 100644 --- a/src/project_x_py/indicators/__init__.py +++ b/src/project_x_py/indicators/__init__.py @@ -202,7 +202,7 @@ ) # Version info -__version__ = "3.0.1" +__version__ = "3.0.2" __author__ = "TexasCoding" diff --git a/src/project_x_py/order_templates.py b/src/project_x_py/order_templates.py index 4e2ee4f..d3b548e 100644 --- a/src/project_x_py/order_templates.py +++ b/src/project_x_py/order_templates.py @@ -136,7 +136,8 @@ async def create_order( if size is None: if risk_amount: # Size = Risk Amount / Stop Distance - instrument = await suite.client.get_instrument(suite.instrument) + # suite.instrument is already an Instrument object after initialization + instrument = suite.instrument tick_value = instrument.tickValue if instrument else 1.0 size = int(risk_amount / (stop_dist * tick_value)) elif risk_percent: @@ -145,7 +146,8 @@ async def create_order( if not account: raise ValueError("No account information available") risk_amount = float(account.balance) * risk_percent - instrument = await suite.client.get_instrument(suite.instrument) + # suite.instrument is already an Instrument object after initialization + instrument = suite.instrument tick_value = instrument.tickValue if instrument else 1.0 size = int(risk_amount / (stop_dist * tick_value)) else: @@ -430,7 +432,8 @@ async def create_order( BracketOrderResponse with order details """ # Get instrument for tick size - instrument = await suite.client.get_instrument(suite.instrument) + # suite.instrument is already an Instrument object after initialization + instrument = suite.instrument if not instrument: raise ValueError("Cannot get instrument details") diff --git a/src/project_x_py/orderbook/analytics.py b/src/project_x_py/orderbook/analytics.py index 82542f5..b8e0e48 100644 --- a/src/project_x_py/orderbook/analytics.py +++ b/src/project_x_py/orderbook/analytics.py @@ -375,6 +375,9 @@ async def get_orderbook_depth(self, price_range: float) -> MarketImpactResponse: "timing_risk": timing_risk, "liquidity_premium": liquidity_premium, "implementation_shortfall": implementation_shortfall, + "total_bid_volume": bid_volume, + "total_ask_volume": ask_volume, + "market_depth_score": confidence_level / 100.0, "timestamp": current_time.isoformat(), } @@ -575,17 +578,77 @@ async def get_statistics(self) -> dict[str, Any]: # Get best prices best_prices = self.orderbook._get_best_bid_ask_unlocked() + # Calculate volume statistics + total_bid_size = ( + self.orderbook.orderbook_bids["volume"].sum() + if not self.orderbook.orderbook_bids.is_empty() + else 0 + ) + total_ask_size = ( + self.orderbook.orderbook_asks["volume"].sum() + if not self.orderbook.orderbook_asks.is_empty() + else 0 + ) + + # Calculate trade statistics + buy_trades = 0 + sell_trades = 0 + total_trade_volume = 0 + if not self.orderbook.recent_trades.is_empty(): + buy_trades = self.orderbook.recent_trades.filter( + pl.col("side") == "buy" + ).height + sell_trades = self.orderbook.recent_trades.filter( + pl.col("side") == "sell" + ).height + total_trade_volume = self.orderbook.recent_trades["volume"].sum() + + avg_trade_size = ( + total_trade_volume / self.orderbook.recent_trades.height + if self.orderbook.recent_trades.height > 0 + else 0 + ) + + # Calculate VWAP + vwap = ( + self.orderbook.vwap_numerator / self.orderbook.vwap_denominator + if self.orderbook.vwap_denominator > 0 + else 0 + ) + + # Get session high/low from trades + session_high = 0 + session_low = 0 + if not self.orderbook.recent_trades.is_empty(): + session_high = self.orderbook.recent_trades["price"].max() + session_low = self.orderbook.recent_trades["price"].min() + # Calculate basic stats stats = { "instrument": self.orderbook.instrument, - "update_count": self.orderbook.level2_update_count, + "level2_update_count": self.orderbook.level2_update_count, "last_update": self.orderbook.last_orderbook_update, "best_bid": best_prices.get("bid"), "best_ask": best_prices.get("ask"), "spread": best_prices.get("spread"), - "bid_levels": self.orderbook.orderbook_bids.height, - "ask_levels": self.orderbook.orderbook_asks.height, + "mid_price": best_prices.get("mid") + if best_prices.get("mid") + else ( + (best_prices.get("bid", 0) + best_prices.get("ask", 0)) / 2 + if best_prices.get("bid") and best_prices.get("ask") + else 0 + ), + "bid_depth": self.orderbook.orderbook_bids.height, + "ask_depth": self.orderbook.orderbook_asks.height, + "total_bid_size": int(total_bid_size), + "total_ask_size": int(total_ask_size), "total_trades": self.orderbook.recent_trades.height, + "buy_trades": buy_trades, + "sell_trades": sell_trades, + "avg_trade_size": avg_trade_size, + "vwap": vwap, + "session_high": session_high, + "session_low": session_low, "order_type_breakdown": dict(self.orderbook.order_type_stats), } diff --git a/src/project_x_py/orderbook/realtime.py b/src/project_x_py/orderbook/realtime.py index a1beb12..33d6a7a 100644 --- a/src/project_x_py/orderbook/realtime.py +++ b/src/project_x_py/orderbook/realtime.py @@ -118,12 +118,16 @@ async def initialize( Args: realtime_client: real-time client instance - subscribe_to_depth: Subscribe to market depth updates - subscribe_to_quotes: Subscribe to quote updates + subscribe_to_depth: Subscribe to market depth updates (kept for compatibility) + subscribe_to_quotes: Subscribe to quote updates (kept for compatibility) Returns: bool: True if initialization successful """ + # Note: subscribe_to_depth and subscribe_to_quotes are kept for API compatibility + # The actual subscription happens at the TradingSuite level + _ = subscribe_to_depth # Acknowledge parameter + _ = subscribe_to_quotes # Acknowledge parameter try: self.realtime_client = realtime_client @@ -528,6 +532,13 @@ async def _process_trade( self.orderbook.vwap_numerator += price * volume self.orderbook.vwap_denominator += volume + # Update memory stats for total volume + self.orderbook.memory_manager.memory_stats["total_volume"] = ( + self.orderbook.memory_manager.memory_stats.get("total_volume", 0) + volume + ) + if volume > self.orderbook.memory_manager.memory_stats.get("largest_trade", 0): + self.orderbook.memory_manager.memory_stats["largest_trade"] = volume + # Create trade record new_trade = pl.DataFrame( { @@ -593,10 +604,6 @@ async def _update_orderbook_level( This method assumes the orderbook lock is already held and modifies the orderbook DataFrames in-place. """ - # Select the appropriate DataFrame - orderbook_df = ( - self.orderbook.orderbook_bids if is_bid else self.orderbook.orderbook_asks - ) side = "bid" if is_bid else "ask" # Update price level history for analytics @@ -609,6 +616,12 @@ async def _update_orderbook_level( } ) + # Get the current DataFrame reference + if is_bid: + orderbook_df = self.orderbook.orderbook_bids + else: + orderbook_df = self.orderbook.orderbook_asks + # Check if price level exists existing = orderbook_df.filter(pl.col("price") == price) @@ -640,7 +653,7 @@ async def _update_orderbook_level( ) orderbook_df = pl.concat([orderbook_df, new_level], how="vertical") - # Update the appropriate DataFrame + # Always update the appropriate DataFrame reference if is_bid: self.orderbook.orderbook_bids = orderbook_df else: diff --git a/src/project_x_py/trading_suite.py b/src/project_x_py/trading_suite.py index b20a393..0df9a03 100644 --- a/src/project_x_py/trading_suite.py +++ b/src/project_x_py/trading_suite.py @@ -430,8 +430,9 @@ async def _initialize(self) -> None: # Initialize optional components if Features.ORDERBOOK in self.config.features: logger.info("Initializing orderbook...") + # Use the actual contract ID for the orderbook to properly match WebSocket updates self.orderbook = OrderBook( - instrument=self._symbol, + instrument=self.instrument.id, # Use contract ID instead of symbol timezone_str=self.config.timezone, project_x=self.client, config=self.config.get_orderbook_config(),