From 02cbfc0b941544e3706ccc2b68c1f55d57ab0087 Mon Sep 17 00:00:00 2001 From: chemavx Date: Thu, 11 Jun 2026 17:12:03 +0000 Subject: [PATCH] fix(executor): keep strong references to fire-and-forget Telegram notification tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asyncio.create_task() results were discarded, and the event loop only holds a weak reference to running tasks — a pending notification could be garbage-collected before executing, silently dropping Telegram messages (documented asyncio pitfall). Route the three notification call sites (trade_opened, trade_legacy_closed, trade_closed) through _notify_in_background(), which stores the task in a module-level set and discards it on completion. Notifications stay fire-and-forget; no business logic changed. Co-Authored-By: Claude Fable 5 --- bot/executor/paper.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/bot/executor/paper.py b/bot/executor/paper.py index 2b75bbe..eae38a5 100644 --- a/bot/executor/paper.py +++ b/bot/executor/paper.py @@ -22,6 +22,19 @@ log = logging.getLogger(__name__) # NOTE: this is a heuristic — see COMMISSION_RATE in bayesian.py for context. POLYMARKET_FEE = 0.02 # 2% +# Strong references to in-flight notification tasks. The event loop only +# keeps a weak reference to tasks created via create_task(), so without this +# set a pending Telegram notification could be garbage-collected before it +# runs. Tasks remove themselves from the set on completion. +_background_tasks: set[asyncio.Task] = set() + + +def _notify_in_background(coro) -> None: + """Fire-and-forget a Telegram notification, keeping the task referenced.""" + task = asyncio.create_task(coro) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) + @dataclass class Trade: @@ -205,7 +218,7 @@ class PaperExecutor: # Persist to DB await self._db.save_trade(trade) - asyncio.create_task( + _notify_in_background( telegram.trade_opened(trade.question, trade.direction, trade.size_usdc, trade.edge_net) ) @@ -226,7 +239,7 @@ class PaperExecutor: "LEGACY_CLOSE market=%s | returned $%.2f to cash | %s", market_id, cost, reason[:80], ) - asyncio.create_task( + _notify_in_background( telegram.trade_legacy_closed(question or market_id, cost, reason) ) return cost @@ -281,7 +294,7 @@ class PaperExecutor: "Closed position in %s, resolution=%.1f payout=$%.2f pnl=%+.2f", market_id, resolution, payout, pnl, ) - asyncio.create_task( + _notify_in_background( telegram.trade_closed(question or market_id, pnl) ) return pnl