feat(notify): Telegram alerts on trade open and close
CI/CD / build-and-push (push) Successful in 7s
CI/CD / build-and-push (push) Successful in 7s
New module bot/notify/telegram.py — httpx async, fire-and-forget via asyncio.create_task, swallows all errors so notifications never affect trade execution. Three alert types: 📈/📉 TRADE ABIERTO — direction, size, edge_net (in execute()) ✅/❌ GANADO/PERDIDO — approx PnL (in close_position()) 🔒 LEGACY CLOSE — recovered capital + reason (in close_legacy_position()) close_position() and close_legacy_position() gain an optional question="" param so the message shows the market name instead of market_id. bot/main.py updated to pass question= to close_legacy_position(). Credentials (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID) read from env vars injected via bot-secrets k8s secret. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
-3
@@ -4,6 +4,7 @@ Paper Trading Executor — simulates order execution without real money.
|
|||||||
Simulates realistic slippage and fees to get accurate paper P&L.
|
Simulates realistic slippage and fees to get accurate paper P&L.
|
||||||
All trades are logged to PostgreSQL for metrics analysis.
|
All trades are logged to PostgreSQL for metrics analysis.
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -12,6 +13,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from bot.risk.manager import Order, Portfolio
|
from bot.risk.manager import Order, Portfolio
|
||||||
from bot.data.db import Database
|
from bot.data.db import Database
|
||||||
|
from bot.notify import telegram
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -183,9 +185,13 @@ class PaperExecutor:
|
|||||||
# Persist to DB
|
# Persist to DB
|
||||||
await self._db.save_trade(trade)
|
await self._db.save_trade(trade)
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
telegram.trade_opened(trade.question, trade.direction, trade.size_usdc, trade.edge_net)
|
||||||
|
)
|
||||||
|
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
async def close_legacy_position(self, market_id: str, reason: str) -> float:
|
async def close_legacy_position(self, market_id: str, reason: str, question: str = "") -> float:
|
||||||
"""
|
"""
|
||||||
Close a paper position flagged by the legacy scan.
|
Close a paper position flagged by the legacy scan.
|
||||||
|
|
||||||
@@ -200,9 +206,12 @@ class PaperExecutor:
|
|||||||
"LEGACY_CLOSE market=%s | returned $%.2f to cash | %s",
|
"LEGACY_CLOSE market=%s | returned $%.2f to cash | %s",
|
||||||
market_id, cost, reason[:80],
|
market_id, cost, reason[:80],
|
||||||
)
|
)
|
||||||
|
asyncio.create_task(
|
||||||
|
telegram.trade_legacy_closed(question or market_id, cost, reason)
|
||||||
|
)
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
async def close_position(self, market_id: str, resolution: float) -> Optional[float]:
|
async def close_position(self, market_id: str, resolution: float, question: str = "") -> Optional[float]:
|
||||||
"""Close a paper position after market resolution.
|
"""Close a paper position after market resolution.
|
||||||
|
|
||||||
resolution: 1.0 if YES won, 0.0 if NO won.
|
resolution: 1.0 if YES won, 0.0 if NO won.
|
||||||
@@ -220,6 +229,10 @@ class PaperExecutor:
|
|||||||
reason=f"market_resolved resolution={resolution:.1f}",
|
reason=f"market_resolved resolution={resolution:.1f}",
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
)
|
)
|
||||||
|
approx_pnl = position_cost * resolution - position_cost
|
||||||
log.info("Closed position in %s, resolution=%.1f", market_id, resolution)
|
log.info("Closed position in %s, resolution=%.1f", market_id, resolution)
|
||||||
|
asyncio.create_task(
|
||||||
|
telegram.trade_closed(question or market_id, approx_pnl)
|
||||||
|
)
|
||||||
# Approximate PnL: settlement value minus cost. Exact value is in close_pnl.
|
# Approximate PnL: settlement value minus cost. Exact value is in close_pnl.
|
||||||
return position_cost * resolution - position_cost
|
return approx_pnl
|
||||||
|
|||||||
+1
-1
@@ -347,7 +347,7 @@ async def run_legacy_scan(
|
|||||||
log.warning("PAPER MODE: auto-closing %d CLOSE_RECOMMENDED position(s)...", n_close)
|
log.warning("PAPER MODE: auto-closing %d CLOSE_RECOMMENDED position(s)...", n_close)
|
||||||
for p in enriched:
|
for p in enriched:
|
||||||
if p["recommendation"] == "CLOSE_RECOMMENDED":
|
if p["recommendation"] == "CLOSE_RECOMMENDED":
|
||||||
recovered = await executor.close_legacy_position(p["market_id"], p["rec_reason"])
|
recovered = await executor.close_legacy_position(p["market_id"], p["rec_reason"], question=p["question"])
|
||||||
log.warning(
|
log.warning(
|
||||||
" AUTO_CLOSED market=%s | $%.2f returned to cash | %s",
|
" AUTO_CLOSED market=%s | $%.2f returned to cash | %s",
|
||||||
p["market_id"], recovered, p["question"][:60],
|
p["market_id"], recovered, p["question"][:60],
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Telegram notifications — fire-and-forget, never blocks trade execution."""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def _send(text: str) -> None:
|
||||||
|
"""POST a message to Telegram. Swallows all errors silently."""
|
||||||
|
if not _TOKEN or not _CHAT_ID:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"https://api.telegram.org/bot{_TOKEN}/sendMessage",
|
||||||
|
json={"chat_id": _CHAT_ID, "text": text},
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
log.warning("Telegram %d: %s", resp.status_code, resp.text[:200])
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Telegram notification failed: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
async def trade_opened(question: str, direction: str, size_usdc: float, edge_net: float) -> None:
|
||||||
|
emoji = "📈" if direction == "BUY_YES" else "📉"
|
||||||
|
await _send(
|
||||||
|
f"{emoji} TRADE ABIERTO\n"
|
||||||
|
f"{question[:80]}\n"
|
||||||
|
f"Dir: {direction} Size: ${size_usdc:.2f} Edge: {edge_net:+.3f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def trade_closed(question: str, pnl: float) -> None:
|
||||||
|
emoji = "✅" if pnl > 0 else "❌"
|
||||||
|
result = "GANADO" if pnl > 0 else "PERDIDO"
|
||||||
|
await _send(
|
||||||
|
f"{emoji} {result}\n"
|
||||||
|
f"{question[:80]}\n"
|
||||||
|
f"PnL: {pnl:+.2f} USDC"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def trade_legacy_closed(question: str, recovered: float, reason: str) -> None:
|
||||||
|
await _send(
|
||||||
|
f"🔒 LEGACY CLOSE\n"
|
||||||
|
f"{question[:80]}\n"
|
||||||
|
f"Recuperado: ${recovered:.2f} | {reason[:60]}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user