1111from typing import Optional , Callable , Dict , Any , List
1212from datetime import datetime , timezone
1313import json
14+ import httpx
1415
1516from agents .polymarket .polymarket import Polymarket
16- from agents .application .position_manager import PositionManager , ActiveTrade
17+ from agents .application .position_manager import PositionManager , ActiveTrade , TradeAction
1718from agents .application .latency_stats import LatencyStats
1819from src .utils .logger import get_logger
1920from src .utils .exceptions import APIError , BotError
@@ -246,6 +247,12 @@ def _detect_dislocation(self, token_id: str) -> Optional[DislocationSignal]:
246247 current = history [- 1 ]
247248 baseline = history [0 ] # Oldest in window
248249
250+ # Avoid division by zero if baseline mid_price is zero (valid but rare for probability prices).
251+ # If baseline is zero we cannot compute a meaningful percentage drop, so skip detection.
252+ if baseline .mid_price <= 0.0 :
253+ logger .debug (f"Baseline mid_price is zero for { token_id } , skipping dislocation detection" )
254+ return None
255+
249256 # Calculate price drop percentage
250257 price_drop_pct = ((baseline .mid_price - current .mid_price ) / baseline .mid_price ) * 100.0
251258
@@ -264,8 +271,10 @@ def _detect_dislocation(self, token_id: str) -> Optional[DislocationSignal]:
264271 if speed_ratio < self .speed_ratio_threshold :
265272 return None # Not fast enough
266273
267- # Determine side: if price dropped, buy (expecting bounce)
268- side = "UP" if "up" in token_id .lower () or current .mid_price < baseline .mid_price else "DOWN"
274+ # Determine side: if price dropped, buy (expecting bounce).
275+ # Use price movement only — do not rely on substring matches in token_id,
276+ # which may produce false positives (e.g. token IDs containing "up" in other words).
277+ side = "UP" if current .mid_price < baseline .mid_price else "DOWN"
269278
270279 t_detect = self ._monotonic_ms ()
271280
@@ -540,11 +549,8 @@ async def _monitor_token(self, token_id: str) -> None:
540549 async def _get_active_market_tokens (self ) -> List [str ]:
541550 """Get token IDs for active 15m markets."""
542551 try :
543- # Use connection pooling for better performance
544- client = self .polymarket ._get_http_client ()
545-
546- # Fetch markets from Gamma API
547- response = client .get (
552+ # Fetch markets from Gamma API (use httpx directly)
553+ response = httpx .get (
548554 f"{ self .polymarket .gamma_url } /markets" ,
549555 params = {"active" : "true" , "closed" : "false" },
550556 timeout = 5
@@ -610,6 +616,9 @@ async def start(self) -> None:
610616 tokens = await self ._get_active_market_tokens ()
611617 logger .info (f"Monitoring { len (tokens )} tokens" )
612618
619+ # Ensure cleanup_task is always defined so finally blocks can safely cancel it
620+ cleanup_task = None
621+
613622 # WebSocket mode: use real-time updates
614623 if self .use_websocket and self .ws_client :
615624 logger .info ("Using WebSocket for real-time price updates (lower latency)" )
@@ -653,8 +662,20 @@ def on_ws_update(update: "OrderBookUpdate"):
653662 except Exception as e :
654663 logger .error (f"Engine error: { e } " )
655664 finally :
665+ # Stop websocket client if running
656666 if self .ws_client :
657- await self .ws_client .stop ()
667+ try :
668+ await self .ws_client .stop ()
669+ except Exception :
670+ logger .exception ("Error stopping ws_client" )
671+ # Ensure cleanup task is cancelled
672+ if cleanup_task is not None :
673+ cleanup_task .cancel ()
674+ try :
675+ await cleanup_task
676+ except asyncio .CancelledError :
677+ pass
678+ self .running = False
658679
659680 else :
660681 # REST polling mode (fallback)
@@ -672,9 +693,14 @@ def on_ws_update(update: "OrderBookUpdate"):
672693 logger .info ("Engine stopped" )
673694 except Exception as e :
674695 logger .error (f"Engine error: { e } " )
675- finally :
676- self .running = False
677- cleanup_task .cancel ()
696+ finally :
697+ self .running = False
698+ if cleanup_task is not None :
699+ cleanup_task .cancel ()
700+ try :
701+ await cleanup_task
702+ except asyncio .CancelledError :
703+ pass
678704
679705 def stop (self ) -> None :
680706 """Stop the engine."""
@@ -702,20 +728,42 @@ def add_size_by_trade_id(self, trade_id: str, additional_size_usdc: float) -> bo
702728 return False
703729
704730 try :
731+ # Execute market order
705732 self .polymarket .execute_order (
706733 price = trade .leg1_price ,
707734 size = additional_size_usdc ,
708735 side = "BUY" ,
709736 token_id = trade .token_id ,
710737 )
711-
738+
739+ # Update trade state via PositionManager to keep totals/idempotency consistent.
740+ # Generate a unique action_id for idempotency tracking.
741+ action_id = f"add_{ trade_id } _{ int (time .time () * 1000 )} "
742+ try :
743+ result = self .position_manager .process_confirmation (
744+ trade_id = trade_id ,
745+ action = TradeAction .ADD ,
746+ action_id = action_id ,
747+ additional_size = additional_size_usdc ,
748+ )
749+ except Exception as e :
750+ logger .exception (f"PositionManager.process_confirmation failed for ADD on { trade_id } : { e } " )
751+ result = {"ok" : False , "message" : str (e )}
752+
753+ if not result .get ("ok" ):
754+ logger .warning (f"ADD processed but position update failed for { trade_id } : { result .get ('message' )} " )
755+
756+ # Refresh trade object (may have been mutated)
757+ trade = self .position_manager .get_trade (trade_id )
758+
712759 self ._log_ms ("ADD_SIZE" , {
713760 "trade_id" : trade_id ,
714- "entry_id" : trade .leg1_entry_id ,
761+ "entry_id" : trade .leg1_entry_id if trade else None ,
715762 "additional_size_usdc" : additional_size_usdc ,
716- "total_size" : trade .total_size ,
763+ "total_size" : trade .total_size if trade else None ,
764+ "status" : trade .status .value if trade else None ,
717765 })
718-
766+
719767 return True
720768 except Exception as e :
721769 logger .error (f"Add size failed: { e } " )
0 commit comments