Skip to content

Commit a0b81ad

Browse files
TexasCodingclaude
andcommitted
fix: critical bracket order fill detection race condition (v3.3.5)
CRITICAL FIX: Resolved race condition in bracket order fill detection that caused operations to hang when market orders filled immediately. Changes: - Added cache check in _wait_for_order_fill to detect already-filled orders - Prevents timeout when market orders fill before event handler registration - Enhanced debug logging for order fill event processing - Ensures proper stop loss and take profit order placement This fix is critical for automated trading strategies using bracket orders, particularly with market orders that execute immediately. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent c2334ff commit a0b81ad

16 files changed

+109
-24
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Migration guides will be provided for all breaking changes
1515
- Semantic versioning (MAJOR.MINOR.PATCH) is strictly followed
1616

17+
## [3.3.5] - 2025-01-24
18+
19+
### Fixed
20+
- **🚨 CRITICAL: Bracket Order Fill Detection**
21+
- Fixed race condition where market orders that fill immediately were not detected
22+
- Added cache check in `_wait_for_order_fill` to detect already-filled orders
23+
- Prevents bracket order operations from hanging on fast-filling market orders
24+
- Ensures proper stop loss and take profit order placement after entry fills
25+
- Critical fix for automated trading strategies using bracket orders
26+
27+
### Improved
28+
- **Order Tracking Debug Logging**
29+
- Enhanced debug logging in fill event handlers for better troubleshooting
30+
- Added detailed order ID extraction and comparison logging
31+
- Improved visibility into event processing for order lifecycle monitoring
32+
1733
## [3.3.4] - 2025-01-23
1834

1935
### Fixed

examples/00_trading_suite_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def main():
4747

4848
# Get stats
4949
print("\n=== Suite Statistics ===")
50-
stats = suite.get_stats()
50+
stats = await suite.get_stats()
5151
print(f"Stats: {stats}")
5252

5353
# Clean disconnect

examples/04_realtime_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def display_memory_stats(data_manager: "RealtimeDataManager") -> None:
9797
"""Display memory usage statistics asynchronously."""
9898
try:
9999
# get_memory_stats is synchronous in async data manager
100-
stats = data_manager.get_memory_stats()
100+
stats = await data_manager.get_memory_stats()
101101
print("\n💾 Memory Statistics:")
102102
print(f" Total Bars Stored: {stats.get('total_bars_stored', 0):,}")
103103
print(f" Ticks Processed: {stats.get('ticks_processed', 0):,}")

examples/06_advanced_orderbook.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ async def demonstrate_comprehensive_stats(orderbook: OrderBook):
420420
print(f" Session Low: ${stats.get('session_low', 0):,.2f}")
421421

422422
# Performance
423-
memory = orderbook.get_memory_stats()
423+
memory = await orderbook.get_memory_stats()
424424
print("\n⚡ Performance:")
425425
print(f" Updates Processed: {stats.get('level2_update_count', 0):,}")
426426
print(f" Memory Cleanups: {memory.get('memory_cleanups', 0)}")

examples/20_statistics_usage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def main():
166166
# Get data manager statistics (v3.3.0 - sync API for data manager)
167167
if hasattr(suite.data, "get_memory_stats"):
168168
data_stats = (
169-
suite.data.get_memory_stats()
169+
await suite.data.get_memory_stats()
170170
) # Note: sync method for data manager
171171
print("\n📊 Data Manager Statistics:")
172172
print(f" Bars processed: {data_stats.get('total_bars', 0)}")

examples/25_dst_handling_demo.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def __init__(self, timezone_name="America/Chicago"):
4747

4848
def _calculate_bar_time(self, timestamp, interval, unit):
4949
"""Standard bar time calculation for demo."""
50+
if self.timezone is None:
51+
raise ValueError("Timezone is not set")
52+
5053
if timestamp.tzinfo is None:
5154
timestamp = self.timezone.localize(timestamp)
5255

@@ -158,12 +161,17 @@ async def demo_fall_back_handling():
158161
try:
159162
# First, try to localize the time
160163
try:
164+
if manager.timezone is None:
165+
raise ValueError("Timezone is not set")
161166
localized_time = manager.timezone.localize(tick_time)
162167
is_dst = localized_time.dst() != timedelta(0)
163168
dst_str = "Yes" if is_dst else "No"
164169
status = "OK"
165170
except pytz.AmbiguousTimeError:
166171
# Duplicate time - use standard time (DST=False)
172+
if manager.timezone is None:
173+
continue
174+
167175
localized_time = manager.timezone.localize(tick_time, is_dst=False)
168176
dst_str = "Ambig"
169177
status = "DISAMBIGUATED"

examples/25_dynamic_resource_limits.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def monitor_resource_usage(suite: TradingSuite, duration_seconds: int = 60
4141

4242
# Get current resource statistics
4343
resource_stats = await suite.data.get_resource_stats()
44-
memory_stats = suite.data.get_memory_stats()
44+
memory_stats = await suite.data.get_memory_stats()
4545

4646
print(f"\n📈 Iteration {iteration} ({time.time() - start_time:.1f}s elapsed)")
4747
print("-" * 40)

examples/99_error_recovery_demo.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ async def demonstrate_bracket_order_recovery():
4848
current_price = await suite.data.get_current_price()
4949
tick_size = 0.25 # NQ tick size
5050

51+
if current_price is None:
52+
raise ValueError("Current price is None")
53+
5154
# Calculate bracket order prices
5255
entry_price = float(current_price - 20 * tick_size) # Enter below market
5356
stop_loss_price = float(current_price - 30 * tick_size) # Risk: $25
@@ -59,6 +62,9 @@ async def demonstrate_bracket_order_recovery():
5962
print(f" Take Profit: ${take_profit_price:.2f}")
6063

6164
try:
65+
if suite.instrument_id is None:
66+
raise ValueError("Instrument ID is None")
67+
6268
# Place a normal bracket order
6369
bracket_response = await suite.orders.place_bracket_order(
6470
contract_id=suite.instrument_id,
@@ -81,7 +87,8 @@ async def demonstrate_bracket_order_recovery():
8187
cancel_results = await suite.orders.cancel_position_orders(
8288
suite.instrument_id
8389
)
84-
print(f" ✓ Cancelled {sum(cancel_results.values())} orders\n")
90+
total_cancelled = sum(v for v in [cancel_results.get(key, 0) for key in ['entry', 'stop', 'target']] if isinstance(v, int))
91+
print(f" ✓ Cancelled {total_cancelled} orders\n")
8592

8693
else:
8794
print(f" ✗ Bracket order failed: {bracket_response.error_message}\n")
@@ -149,13 +156,19 @@ async def demonstrate_position_order_recovery():
149156
# Demonstrate enhanced cancellation with error tracking
150157
print("1. Testing enhanced position order cancellation...")
151158

159+
if suite.instrument_id is None:
160+
raise ValueError("Instrument ID is None")
161+
152162
# First, check if there are any existing orders
153-
position_orders = suite.orders.get_position_orders(suite.instrument_id)
163+
position_orders = await suite.orders.get_position_orders(suite.instrument_id)
154164
total_orders = sum(len(orders) for orders in position_orders.values())
155165

156166
if total_orders > 0:
157167
print(f" Found {total_orders} existing orders")
158168

169+
if suite.instrument_id is None:
170+
raise ValueError("Instrument ID is None")
171+
159172
# Cancel with enhanced error tracking
160173
cancel_results = await suite.orders.cancel_position_orders(
161174
suite.instrument_id
@@ -167,9 +180,10 @@ async def demonstrate_position_order_recovery():
167180
print(f" Target orders: {cancel_results.get('target', 0)}")
168181
print(f" Failed: {cancel_results.get('failed', 0)}")
169182

170-
if cancel_results.get("errors"):
171-
print(f" Errors: {len(cancel_results['errors'])}")
172-
for error in cancel_results["errors"][:3]: # Show first 3 errors
183+
errors = cancel_results.get("errors")
184+
if errors and isinstance(errors, list):
185+
print(f" Errors: {len(errors)}")
186+
for error in errors[:3]: # Show first 3 errors
173187
print(f" - {error}")
174188
else:
175189
print(" No existing orders found")

examples/advanced_dataframe_operations.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def demonstrate_lazy_operations(suite: TradingSuite) -> None:
7171

7272
execution_time = (time.time() - start_time) * 1000
7373
print(
74-
f" Filtered {len(data_5m)}{len(filtered_data) if filtered_data else 0} bars"
74+
f" Filtered {len(data_5m)}{len(filtered_data) if filtered_data is not None else 0} bars"
7575
)
7676
print(f" Execution time: {execution_time:.2f} ms")
7777
print(" Memory efficient: Only loaded selected columns")
@@ -384,7 +384,7 @@ async def main():
384384
)
385385

386386
print(
387-
f"✓ Connected to {suite.instrument} with {len(suite.timeframes)} timeframes"
387+
f"✓ Connected to {suite.instrument} with {len(['1min', '5min', '15min'])} timeframes"
388388
)
389389

390390
# Wait for some real-time data
@@ -395,7 +395,7 @@ async def main():
395395
data_stats = {}
396396
for tf in ["1min", "5min", "15min"]:
397397
data = await suite.data.get_data(tf)
398-
data_stats[tf] = len(data) if data else 0
398+
data_stats[tf] = len(data) if data is not None else 0
399399

400400
print(f"Data availability: {data_stats}")
401401

@@ -419,11 +419,11 @@ async def main():
419419
print("=" * 60)
420420

421421
# Memory statistics
422-
memory_stats = suite.data.get_memory_stats()
423-
print(f"Total bars processed: {memory_stats.get('bars_processed', 0)}")
424-
print(f"Ticks processed: {memory_stats.get('ticks_processed', 0)}")
425-
print(f"Memory usage: {memory_stats.get('memory_usage_mb', 0):.2f} MB")
426-
print(f"Buffer utilization: {memory_stats.get('buffer_utilization', 0):.1%}")
422+
memory_stats = await suite.data.get_memory_stats()
423+
print(f"Total bars processed: {memory_stats['bars_processed']}")
424+
print(f"Ticks processed: {memory_stats['ticks_processed']}")
425+
print(f"Memory usage: {memory_stats['memory_usage_mb']:.2f} MB")
426+
print(f"Buffer utilization: {memory_stats['buffer_utilization']:.1%}")
427427

428428
print("\n✓ DataFrame optimization demonstration completed successfully!")
429429

examples/dataframe_optimization_benchmark.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ async def benchmark_basic_operations(
129129
start_memory = get_memory_usage()
130130

131131
lazy_df = await manager.get_lazy_data("1min")
132+
if lazy_df is None:
133+
raise ValueError("Lazy DataFrame is None")
134+
132135
filtered = await manager.apply_lazy_operations(
133136
lazy_df, [("filter", pl.col("volume") > 1500)]
134137
)
@@ -147,6 +150,9 @@ async def benchmark_basic_operations(
147150
start_memory = get_memory_usage()
148151

149152
lazy_df = await manager.get_lazy_data("1min")
153+
if lazy_df is None:
154+
raise ValueError("Lazy DataFrame is None")
155+
150156
complex_result = await manager.apply_lazy_operations(
151157
lazy_df,
152158
[
@@ -287,6 +293,8 @@ async def benchmark_optimization_effectiveness(
287293
]
288294

289295
lazy_df = await manager.get_lazy_data("1min")
296+
if lazy_df is None:
297+
raise ValueError("Lazy DataFrame is None")
290298

291299
# Without optimization
292300
start_time = time.time()
@@ -299,6 +307,9 @@ async def benchmark_optimization_effectiveness(
299307

300308
# With optimization
301309
lazy_df = await manager.get_lazy_data("1min")
310+
if lazy_df is None:
311+
raise ValueError("Lazy DataFrame is None")
312+
302313
start_time = time.time()
303314
start_memory = get_memory_usage()
304315
result_opt = await manager.apply_lazy_operations(lazy_df, operations, optimize=True)
@@ -346,6 +357,9 @@ async def benchmark_memory_optimization(
346357

347358
# Execute memory-intensive operations
348359
lazy_df = await manager.get_lazy_data("1sec")
360+
if lazy_df is None:
361+
raise ValueError("Lazy DataFrame is None")
362+
349363
result = await manager.apply_lazy_operations(
350364
lazy_df,
351365
[

0 commit comments

Comments
 (0)