Skip to content

Commit 24d8b22

Browse files
authored
Merge pull request #8 from TexasCoding/work_on_example_strategy
Work on example strategy
2 parents d63f854 + 0e8c3bd commit 24d8b22

File tree

4 files changed

+263
-25
lines changed

4 files changed

+263
-25
lines changed

examples/06_multi_timeframe_strategy.py

Lines changed: 222 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
Date: July 2025
2323
"""
2424

25+
import signal
26+
import sys
2527
import time
2628
from decimal import Decimal
2729

@@ -344,23 +346,106 @@ def display_strategy_analysis(strategy):
344346
return signal_data
345347

346348

349+
# Global variables for cleanup
350+
_cleanup_managers = {}
351+
_cleanup_initiated = False
352+
353+
def _emergency_cleanup(signum=None, frame=None):
354+
"""Emergency cleanup function called on signal interruption."""
355+
global _cleanup_initiated
356+
if _cleanup_initiated:
357+
print("\n🚨 Already cleaning up, please wait...")
358+
return
359+
360+
_cleanup_initiated = True
361+
362+
if signum:
363+
signal_name = signal.Signals(signum).name
364+
print(f"\n🚨 Received {signal_name} signal - initiating emergency cleanup!")
365+
else:
366+
print("\n🚨 Initiating emergency cleanup!")
367+
368+
if _cleanup_managers:
369+
print("⚠️ Emergency position and order cleanup in progress...")
370+
371+
try:
372+
order_manager = _cleanup_managers.get("order_manager")
373+
position_manager = _cleanup_managers.get("position_manager")
374+
data_manager = _cleanup_managers.get("data_manager")
375+
376+
if order_manager and position_manager:
377+
# Get current state
378+
positions = position_manager.get_all_positions()
379+
orders = order_manager.search_open_orders()
380+
381+
if positions or orders:
382+
print(f"🚫 Emergency: Cancelling {len(orders)} orders and closing {len(positions)} positions...")
383+
384+
# Cancel all orders immediately
385+
for order in orders:
386+
try:
387+
order_manager.cancel_order(order.id)
388+
print(f" ✅ Cancelled order {order.id}")
389+
except:
390+
print(f" ❌ Failed to cancel order {order.id}")
391+
392+
# Close all positions with market orders
393+
for pos in positions:
394+
try:
395+
close_side = 1 if pos.type == 1 else 0
396+
close_response = order_manager.place_market_order(
397+
contract_id=pos.contractId,
398+
side=close_side,
399+
size=pos.size
400+
)
401+
if close_response.success:
402+
print(f" ✅ Emergency close order: {close_response.order_id}")
403+
except:
404+
print(f" ❌ Failed to close position {pos.contractId}")
405+
406+
print("⏳ Waiting 3 seconds for emergency orders to process...")
407+
time.sleep(3)
408+
else:
409+
print("✅ No positions or orders to clean up")
410+
411+
# Stop data feed
412+
if data_manager:
413+
try:
414+
data_manager.stop_realtime_feed()
415+
print("🧹 Real-time feed stopped")
416+
except:
417+
pass
418+
419+
except Exception as e:
420+
print(f"❌ Emergency cleanup error: {e}")
421+
422+
print("🚨 Emergency cleanup completed - check your trading platform!")
423+
sys.exit(1)
424+
347425
def wait_for_user_confirmation(message: str) -> bool:
348426
"""Wait for user confirmation before proceeding."""
349427
print(f"\n⚠️ {message}")
350428
try:
351429
response = input("Continue? (y/N): ").strip().lower()
352430
return response == "y"
353-
except EOFError:
354-
# Handle EOF when input is piped (default to no for safety)
355-
print("N (EOF detected - defaulting to No for safety)")
431+
except (EOFError, KeyboardInterrupt):
432+
# Handle EOF when input is piped or Ctrl+C during input
433+
print("\nN (Interrupted - defaulting to No for safety)")
356434
return False
357435

358436

359437
def main():
360438
"""Demonstrate multi-timeframe trading strategy."""
439+
global _cleanup_managers
440+
441+
# Register signal handlers for emergency cleanup
442+
signal.signal(signal.SIGINT, _emergency_cleanup) # Ctrl+C
443+
signal.signal(signal.SIGTERM, _emergency_cleanup) # Termination signal
444+
361445
logger = setup_logging(level="INFO")
362446
print("🚀 Multi-Timeframe Trading Strategy Example")
363447
print("=" * 60)
448+
print("📋 Emergency cleanup registered (Ctrl+C will close positions/orders)")
364449

365450
# Safety warning
366451
print("⚠️ WARNING: This strategy can place REAL ORDERS!")
@@ -406,18 +491,25 @@ def main():
406491
data_manager = trading_suite["data_manager"]
407492
order_manager = trading_suite["order_manager"]
408493
position_manager = trading_suite["position_manager"]
494+
495+
# Store managers for emergency cleanup
496+
_cleanup_managers["data_manager"] = data_manager
497+
_cleanup_managers["order_manager"] = order_manager
498+
_cleanup_managers["position_manager"] = position_manager
409499

410500
print("✅ Trading suite created successfully")
411501
print(f" Timeframes: {', '.join(timeframes)}")
502+
print("🛡️ Emergency cleanup protection activated")
412503

413504
except Exception as e:
414505
print(f"❌ Failed to create trading suite: {e}")
415506
return False
416507

417-
# Initialize with historical data
508+
# Initialize with historical data (need enough for 50-period SMA on 4hr timeframe)
418509
print("\n📚 Initializing with historical data...")
419-
if data_manager.initialize(initial_days=10):
420-
print("✅ Historical data loaded (10 days)")
510+
# 50 periods * 4 hours = 200 hours ≈ 8.3 days, so load 15 days to be safe
511+
if data_manager.initialize(initial_days=15):
512+
print("✅ Historical data loaded (15 days)")
421513
else:
422514
print("❌ Failed to load historical data")
423515
return False
@@ -537,6 +629,7 @@ def main():
537629

538630
except KeyboardInterrupt:
539631
print("\n⏹️ Strategy monitoring stopped by user")
632+
# Signal handler will take care of cleanup
540633

541634
# Final analysis and statistics
542635
print("\n" + "=" * 50)
@@ -560,11 +653,24 @@ def main():
560653
print(" Position Details:")
561654
for pos in final_positions:
562655
direction = "LONG" if pos.type == 1 else "SHORT"
563-
pnl_info = position_manager.get_position_pnl(pos.contractId)
564-
pnl = pnl_info.get("unrealized_pnl", 0) if pnl_info else 0
565-
print(
566-
f" {pos.contractId}: {direction} {pos.size} @ ${pos.averagePrice:.2f} (P&L: ${pnl:+.2f})"
567-
)
656+
657+
# Get current price for P&L calculation
658+
try:
659+
current_price = data_manager.get_current_price()
660+
if current_price:
661+
pnl_info = position_manager.calculate_position_pnl(pos, current_price)
662+
pnl = pnl_info.get("unrealized_pnl", 0) if pnl_info else 0
663+
print(
664+
f" {pos.contractId}: {direction} {pos.size} @ ${pos.averagePrice:.2f} (P&L: ${pnl:+.2f})"
665+
)
666+
else:
667+
print(
668+
f" {pos.contractId}: {direction} {pos.size} @ ${pos.averagePrice:.2f} (P&L: N/A)"
669+
)
670+
except Exception as e:
671+
print(
672+
f" {pos.contractId}: {direction} {pos.size} @ ${pos.averagePrice:.2f} (P&L: Error - {e})"
673+
)
568674

569675
# Show final signal analysis
570676
print("\n🧠 Final Strategy Analysis:")
@@ -599,19 +705,121 @@ def main():
599705

600706
except KeyboardInterrupt:
601707
print("\n⏹️ Example interrupted by user")
708+
# Signal handler will handle emergency cleanup
602709
return False
603710
except Exception as e:
604711
logger.error(f"❌ Multi-timeframe strategy example failed: {e}")
605712
print(f"❌ Error: {e}")
606713
return False
607714
finally:
608-
# Cleanup
715+
# Comprehensive cleanup - close positions and cancel orders
716+
cleanup_performed = False
717+
718+
if "order_manager" in locals() and "position_manager" in locals():
719+
try:
720+
print("\n" + "=" * 50)
721+
print("🧹 STRATEGY CLEANUP")
722+
print("=" * 50)
723+
724+
# Get current positions and orders
725+
final_positions = position_manager.get_all_positions()
726+
final_orders = order_manager.search_open_orders()
727+
728+
if final_positions or final_orders:
729+
print(f"⚠️ Found {len(final_positions)} open positions and {len(final_orders)} open orders")
730+
print(" For safety, all positions and orders should be closed when exiting.")
731+
732+
# Ask for user confirmation to close everything
733+
if wait_for_user_confirmation("Close all positions and cancel all orders?"):
734+
cleanup_performed = True
735+
736+
# Cancel all open orders first
737+
if final_orders:
738+
print(f"\n🚫 Cancelling {len(final_orders)} open orders...")
739+
cancelled_count = 0
740+
for order in final_orders:
741+
try:
742+
if order_manager.cancel_order(order.id):
743+
cancelled_count += 1
744+
print(f" ✅ Cancelled order {order.id}")
745+
else:
746+
print(f" ❌ Failed to cancel order {order.id}")
747+
except Exception as e:
748+
print(f" ❌ Error cancelling order {order.id}: {e}")
749+
750+
print(f" 📊 Successfully cancelled {cancelled_count}/{len(final_orders)} orders")
751+
752+
# Close all open positions
753+
if final_positions:
754+
print(f"\n📤 Closing {len(final_positions)} open positions...")
755+
closed_count = 0
756+
757+
for pos in final_positions:
758+
try:
759+
direction = "LONG" if pos.type == 1 else "SHORT"
760+
print(f" 🎯 Closing {direction} {pos.size} {pos.contractId} @ ${pos.averagePrice:.2f}")
761+
762+
# Get current market price for market order
763+
current_price = data_manager.get_current_price() if "data_manager" in locals() else None
764+
765+
# Close position with market order (opposite side)
766+
close_side = 1 if pos.type == 1 else 0 # Opposite of position type
767+
768+
close_response = order_manager.place_market_order(
769+
contract_id=pos.contractId,
770+
side=close_side,
771+
size=pos.size
772+
)
773+
774+
if close_response.success:
775+
closed_count += 1
776+
print(f" ✅ Close order placed: {close_response.order_id}")
777+
else:
778+
print(f" ❌ Failed to place close order: {close_response.error_message}")
779+
780+
except Exception as e:
781+
print(f" ❌ Error closing position {pos.contractId}: {e}")
782+
783+
print(f" 📊 Successfully placed {closed_count}/{len(final_positions)} close orders")
784+
785+
# Give orders time to fill
786+
if closed_count > 0:
787+
print(" ⏳ Waiting 5 seconds for orders to fill...")
788+
time.sleep(5)
789+
790+
# Check final status
791+
remaining_positions = position_manager.get_all_positions()
792+
if remaining_positions:
793+
print(f" ⚠️ {len(remaining_positions)} positions still open - monitor manually")
794+
else:
795+
print(" ✅ All positions successfully closed")
796+
else:
797+
print(" ℹ️ Cleanup skipped by user - positions and orders remain open")
798+
print(" ⚠️ IMPORTANT: Monitor your positions manually!")
799+
else:
800+
print("✅ No open positions or orders to clean up")
801+
cleanup_performed = True
802+
803+
except Exception as e:
804+
print(f"❌ Error during cleanup: {e}")
805+
806+
# Stop real-time feed
609807
if "data_manager" in locals():
610808
try:
611809
data_manager.stop_realtime_feed()
612-
print("🧹 Real-time feed stopped")
810+
print("\n🧹 Real-time feed stopped")
613811
except Exception as e:
614-
print(f"⚠️ Cleanup warning: {e}")
812+
print(f"⚠️ Feed stop warning: {e}")
813+
814+
# Final safety message
815+
if not cleanup_performed:
816+
print("\n" + "⚠️ " * 20)
817+
print("🚨 IMPORTANT SAFETY NOTICE:")
818+
print(" - Open positions and orders were NOT automatically closed")
819+
print(" - Please check your trading platform immediately")
820+
print(" - Manually close any unwanted positions or orders")
821+
print(" - Monitor your account for any unexpected activity")
822+
print("⚠️ " * 20)
615823

616824

617825
if __name__ == "__main__":

examples/07_technical_indicators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def create_comprehensive_analysis(data):
440440
total_signals += 1
441441

442442
# Momentum Analysis
443-
rsi = row.get("rsi", 0)
443+
rsi = row.get("rsi_14", 0)
444444

445445
print("\n⚡ Momentum Indicators:")
446446
if 30 < rsi < 70:

src/project_x_py/position_manager.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,23 +274,28 @@ def _process_position_data(self, position_data: dict):
274274
- type=1 means "Long", type=2 means "Short"
275275
"""
276276
try:
277-
# According to ProjectX docs, the payload IS the position data directly
278-
# No need to extract from "data" field
277+
# Handle wrapped position data from real-time updates
278+
# Real-time updates come as: {"action": 1, "data": {position_data}}
279+
# But direct API calls might provide raw position data
280+
actual_position_data = position_data
281+
if "action" in position_data and "data" in position_data:
282+
actual_position_data = position_data["data"]
283+
self.logger.debug(f"Extracted position data from wrapper: action={position_data.get('action')}")
279284

280285
# Validate payload format
281-
if not self._validate_position_payload(position_data):
282-
self.logger.error(f"Invalid position payload format: {position_data}")
286+
if not self._validate_position_payload(actual_position_data):
287+
self.logger.error(f"Invalid position payload format: {actual_position_data}")
283288
return
284289

285-
contract_id = position_data.get("contractId")
290+
contract_id = actual_position_data.get("contractId")
286291
if not contract_id:
287-
self.logger.error(f"No contract ID found in {position_data}")
292+
self.logger.error(f"No contract ID found in {actual_position_data}")
288293
return
289294

290295
# Check if this is a position closure
291296
# Position is closed when size == 0 (not when type == 0)
292297
# type=0 means "Undefined" according to PositionType enum, not closed
293-
position_size = position_data.get("size", 0)
298+
position_size = actual_position_data.get("size", 0)
294299
is_position_closed = position_size == 0
295300

296301
# Get the old position before updating
@@ -309,11 +314,11 @@ def _process_position_data(self, position_data: dict):
309314
self.order_manager.on_position_closed(contract_id)
310315

311316
# Trigger position_closed callbacks with the closure data
312-
self._trigger_callbacks("position_closed", {"data": position_data})
317+
self._trigger_callbacks("position_closed", {"data": actual_position_data})
313318
else:
314319
# Position is open/updated - create or update position
315320
# ProjectX payload structure matches our Position model fields
316-
position = Position(**position_data)
321+
position = Position(**actual_position_data)
317322
self.tracked_positions[contract_id] = position
318323

319324
# Synchronize orders - update order sizes if position size changed
@@ -330,7 +335,7 @@ def _process_position_data(self, position_data: dict):
330335
self.position_history[contract_id].append(
331336
{
332337
"timestamp": datetime.now(),
333-
"position": position_data.copy(),
338+
"position": actual_position_data.copy(),
334339
"size_change": position_size - old_size,
335340
}
336341
)

0 commit comments

Comments
 (0)