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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,25 @@ 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
- **Feature Flags**: Easy enabling of optional components like orderbook and risk manager
- **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?

Expand All @@ -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\")
```

Expand Down Expand Up @@ -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:

Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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 = "3.0.1"
version = "3.0.1"
release = "3.0.2"
version = "3.0.2"

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

Expand Down
23 changes: 11 additions & 12 deletions examples/03_position_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"]
Expand Down
81 changes: 51 additions & 30 deletions examples/04_realtime_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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}")
Expand All @@ -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:")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
Loading
Loading