Guest User

Untitled

a guest
Apr 13th, 2026
1,341
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.90 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Polymarket BTC Up/Down 5-minute trading bot.
  4. Dependencias: pip install py-clob-client websocket-client requests eth-account
  5. """
  6.  
  7. import json
  8. import sys
  9. import time
  10. import threading
  11. from datetime import datetime
  12. from math import floor
  13.  
  14. import requests
  15. import websocket
  16. from py_clob_client.client import ClobClient
  17. from py_clob_client.clob_types import OrderArgs, OrderType
  18. from py_clob_client.order_builder.constants import BUY
  19.  
  20. # ─── Configuración ────────────────────────────────────────────────────────────
  21.  
  22. TRIGGER_PRICE = 0.90
  23. BUY_AMOUNT = 5.0
  24. PRIVATE_KEY = "TU_PRIVATE_KEY_AQUI"
  25. PROXY_WALLET = "TU_PROXY_WALLET_AQUI"
  26. CHAIN_ID = 137
  27. SIGNATURE_TYPE = 2
  28.  
  29. HOST = "https://clob.polymarket.com"
  30. GAMMA_API = "https://gamma-api.polymarket.com"
  31. WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
  32.  
  33. # ─── Utilidades ───────────────────────────────────────────────────────────────
  34.  
  35. def ts() -> str:
  36. return datetime.now().strftime("%H:%M:%S")
  37.  
  38. def log(icon: str, msg: str) -> None:
  39. print(f"[{ts()}] {icon} {msg}")
  40.  
  41. def current_window_ts() -> int:
  42. return (int(time.time()) // 300) * 300
  43.  
  44. def slug_for(window_ts: int) -> str:
  45. return f"btc-updown-5m-{window_ts}"
  46.  
  47. def window_end(window_ts: int) -> float:
  48. return float(window_ts + 300)
  49.  
  50. # ─── Carga de mercado ─────────────────────────────────────────────────────────
  51.  
  52. def load_market(slug: str) -> dict | None:
  53. """
  54. Devuelve {"up": token_id, "down": token_id} o None si falla.
  55. """
  56. url = f"{GAMMA_API}/events?slug={slug}"
  57. try:
  58. resp = requests.get(url, timeout=10)
  59. resp.raise_for_status()
  60. data = resp.json()
  61. if not data:
  62. return None
  63.  
  64. market = data[0]["markets"][0]
  65.  
  66. # Intento principal: campo "tokens"
  67. tokens = market.get("tokens")
  68. if tokens:
  69. result = {}
  70. for t in tokens:
  71. outcome = t.get("outcome", "").upper()
  72. token_id = t.get("token_id") or t.get("tokenId")
  73. if outcome in ("UP", "DOWN") and token_id:
  74. result[outcome.lower()] = token_id
  75. if "up" in result and "down" in result:
  76. return result
  77.  
  78. # Fallback: clobTokenIds + outcomes
  79. clob_ids = market.get("clobTokenIds", [])
  80. outcomes = market.get("outcomes", [])
  81.  
  82. if isinstance(clob_ids, str):
  83. clob_ids = json.loads(clob_ids)
  84. if isinstance(outcomes, str):
  85. outcomes = json.loads(outcomes)
  86.  
  87. if clob_ids and outcomes and len(clob_ids) == len(outcomes):
  88. result = {}
  89. for oc, tid in zip(outcomes, clob_ids):
  90. oc_up = oc.upper()
  91. if oc_up in ("UP", "DOWN"):
  92. result[oc_up.lower()] = tid
  93. if "up" in result and "down" in result:
  94. return result
  95.  
  96. log("⚠", f"No se pudieron extraer token IDs del mercado {slug}")
  97. return None
  98.  
  99. except Exception as exc:
  100. log("✗", f"Error cargando mercado {slug}: {exc}")
  101. return None
  102.  
  103. # ─── Price Feed (WebSocket) ───────────────────────────────────────────────────
  104.  
  105. class PriceFeed:
  106. """
  107. Mantiene los mid-prices de UP y DOWN en tiempo real.
  108. Corre en un hilo daemon con reconexión automática.
  109. """
  110.  
  111. def __init__(self, up_id: str, down_id: str) -> None:
  112. self.up_id = up_id
  113. self.down_id = down_id
  114. self.prices: dict[str, float | None] = {"up": None, "down": None}
  115. self._id_to_side = {up_id: "up", down_id: "down"}
  116. self._stop = threading.Event()
  117. self._ws = None
  118. self._thread = threading.Thread(target=self._run, daemon=True)
  119.  
  120. def start(self) -> None:
  121. self._thread.start()
  122.  
  123. def stop(self) -> None:
  124. self._stop.set()
  125. if self._ws:
  126. try:
  127. self._ws.close()
  128. except Exception:
  129. pass
  130.  
  131. def _mid(self, best_bid, best_ask) -> float | None:
  132. try:
  133. b = float(best_bid) if best_bid else None
  134. a = float(best_ask) if best_ask else None
  135. if b is not None and a is not None:
  136. return (b + a) / 2
  137. return b or a
  138. except (TypeError, ValueError):
  139. return None
  140.  
  141. def _process_message(self, raw: str) -> None:
  142. try:
  143. events = json.loads(raw)
  144. if not isinstance(events, list):
  145. events = [events]
  146.  
  147. for event in events:
  148. etype = event.get("event_type") or event.get("type", "")
  149.  
  150. if etype == "book":
  151. asset_id = event.get("asset_id", "")
  152. side = self._id_to_side.get(asset_id)
  153. if side:
  154. bids = event.get("bids", [])
  155. asks = event.get("asks", [])
  156. best_bid = bids[0]["price"] if bids else None
  157. best_ask = asks[0]["price"] if asks else None
  158. mid = self._mid(best_bid, best_ask)
  159. if mid is not None:
  160. self.prices[side] = mid
  161.  
  162. elif etype == "price_change":
  163. for change in event.get("price_changes", []):
  164. asset_id = change.get("asset_id", "")
  165. side = self._id_to_side.get(asset_id)
  166. if side:
  167. mid = self._mid(
  168. change.get("best_bid"),
  169. change.get("best_ask"),
  170. )
  171. if mid is not None:
  172. self.prices[side] = mid
  173.  
  174. elif etype == "best_bid_ask":
  175. asset_id = event.get("asset_id", "")
  176. side = self._id_to_side.get(asset_id)
  177. if side:
  178. mid = self._mid(
  179. event.get("best_bid"),
  180. event.get("best_ask"),
  181. )
  182. if mid is not None:
  183. self.prices[side] = mid
  184.  
  185. except Exception as exc:
  186. log("⚠", f"Error procesando mensaje WS: {exc}")
  187.  
  188. def _run(self) -> None:
  189. subscribe_msg = json.dumps({
  190. "assets_ids": [self.up_id, self.down_id],
  191. "type": "market",
  192. "custom_feature_enabled": True,
  193. })
  194.  
  195. while not self._stop.is_set():
  196. try:
  197. ws = websocket.WebSocketApp(
  198. WS_URL,
  199. on_open=lambda ws: ws.send(subscribe_msg),
  200. on_message=lambda ws, msg: self._process_message(msg),
  201. on_error=lambda ws, err: log("⚠", f"WS error: {err}"),
  202. on_close=lambda ws, code, msg: None,
  203. )
  204. self._ws = ws
  205. ws.run_forever(ping_interval=20, ping_timeout=10)
  206. except Exception as exc:
  207. log("⚠", f"WS caído: {exc}")
  208.  
  209. if not self._stop.is_set():
  210. time.sleep(3)
  211.  
  212. # ─── Cliente CLOB ─────────────────────────────────────────────────────────────
  213.  
  214. def build_client() -> ClobClient:
  215. client = ClobClient(
  216. HOST,
  217. key=PRIVATE_KEY,
  218. chain_id=CHAIN_ID,
  219. signature_type=SIGNATURE_TYPE,
  220. funder=PROXY_WALLET,
  221. )
  222. client.set_api_creds(client.create_or_derive_api_creds())
  223. return client
  224.  
  225. # ─── Orden de compra ──────────────────────────────────────────────────────────
  226.  
  227. def place_buy(client: ClobClient, token_id: str, side_label: str) -> bool:
  228. shares = floor((BUY_AMOUNT / TRIGGER_PRICE) * 100) / 100
  229. log("🛒", f"Comprando {shares} shares de {side_label} @ {TRIGGER_PRICE}")
  230. try:
  231. order = client.create_order(
  232. OrderArgs(
  233. price=TRIGGER_PRICE,
  234. size=shares,
  235. side=BUY,
  236. token_id=token_id,
  237. )
  238. )
  239. resp = client.post_order(order, OrderType.GTC)
  240. log("✅", f"Orden enviada: {resp}")
  241. return True
  242. except Exception as exc:
  243. log("✗", f"Error al colocar orden: {exc}")
  244. return False
  245.  
  246. # ─── Ciclo de ventana ─────────────────────────────────────────────────────────
  247.  
  248. def run_window(client: ClobClient, window_ts: int) -> int:
  249. """
  250. Ejecuta una ventana de 5 minutos.
  251. Devuelve el timestamp de la siguiente ventana.
  252. """
  253. slug = slug_for(window_ts)
  254. end = window_end(window_ts)
  255.  
  256. log("🕐", f"Ventana activa: {slug} (cierre en {int(end - time.time())}s)")
  257.  
  258. # ── Cargar mercado con reintentos ────────────────────────────────────────
  259. tokens = None
  260. while tokens is None:
  261. if time.time() >= end:
  262. log("⏩", "Ventana expiró antes de cargar el mercado, avanzando")
  263. return window_ts + 300
  264.  
  265. # Si la ventana cambió mientras esperábamos, actualizar
  266. new_ts = current_window_ts()
  267. if new_ts != window_ts:
  268. log("🔄", "Ventana cambió durante la carga, actualizando slug")
  269. window_ts = new_ts
  270. slug = slug_for(window_ts)
  271. end = window_end(window_ts)
  272.  
  273. tokens = load_market(slug)
  274. if tokens is None:
  275. log("⏳", f"Mercado no disponible, reintentando en 3s…")
  276. time.sleep(3)
  277.  
  278. log("📋", f"UP token: {tokens['up']}")
  279. log("📋", f"DOWN token: {tokens['down']}")
  280.  
  281. # ── Iniciar price feed ───────────────────────────────────────────────────
  282. feed = PriceFeed(tokens["up"], tokens["down"])
  283. feed.start()
  284.  
  285. # ── Esperar primeros precios (máx 5s) ────────────────────────────────────
  286. deadline = time.time() + 5
  287. while time.time() < deadline:
  288. if feed.prices["up"] is not None and feed.prices["down"] is not None:
  289. break
  290. time.sleep(0.1)
  291. else:
  292. log("⚠", "No se recibieron precios en 5s, continuando de todos modos")
  293.  
  294. # ── Monitorización ───────────────────────────────────────────────────────
  295. bought = False
  296. log("👀", "Monitorizando precios…")
  297.  
  298. while not bought and time.time() < end:
  299. p_up = feed.prices["up"]
  300. p_down = feed.prices["down"]
  301.  
  302. up_str = f"{p_up:.4f}" if p_up is not None else "----"
  303. down_str = f"{p_down:.4f}" if p_down is not None else "----"
  304. remaining = max(0, int(end - time.time()))
  305.  
  306. print(
  307. f"\r[{ts()}] 📊 UP {up_str} DOWN {down_str} "
  308. f"⏱ {remaining:>3}s restantes ",
  309. end="",
  310. flush=True,
  311. )
  312.  
  313. # Comprobar trigger
  314. for side, price, token_id in (
  315. ("UP", p_up, tokens["up"]),
  316. ("DOWN", p_down, tokens["down"]),
  317. ):
  318. if price is not None and price >= TRIGGER_PRICE:
  319. print() # nueva línea tras el \r
  320. log("🎯", f"¡TRIGGER! {side} @ {price:.4f} >= {TRIGGER_PRICE}")
  321. if place_buy(client, token_id, side):
  322. bought = True
  323. break
  324.  
  325. if not bought:
  326. time.sleep(0.05)
  327.  
  328. print() # nueva línea final
  329.  
  330. if not bought:
  331. log("⌛", "Ventana cerrada sin trigger")
  332.  
  333. feed.stop()
  334. return window_ts + 300
  335.  
  336. # ─── Main ─────────────────────────────────────────────────────────────────────
  337.  
  338. def main() -> None:
  339. log("🚀", "Iniciando bot Polymarket BTC Up/Down 5m")
  340. log("⚙", f"Trigger: {TRIGGER_PRICE} | Importe: ${BUY_AMOUNT}")
  341.  
  342. try:
  343. client = build_client()
  344. log("🔑", "Cliente CLOB autenticado correctamente")
  345. except Exception as exc:
  346. log("✗", f"Error al autenticar cliente CLOB: {exc}")
  347. sys.exit(1)
  348.  
  349. next_ts = current_window_ts()
  350.  
  351. while True:
  352. try:
  353. next_ts = run_window(client, next_ts)
  354. # Pequeña espera para asegurarse de que la ventana ya está activa
  355. now = time.time()
  356. if next_ts > now:
  357. wait = next_ts - now
  358. log("💤", f"Esperando {wait:.1f}s al inicio de la próxima ventana…")
  359. time.sleep(wait)
  360. except KeyboardInterrupt:
  361. log("🛑", "Bot detenido por el usuario")
  362. sys.exit(0)
  363. except Exception as exc:
  364. log("✗", f"Error inesperado: {exc}")
  365. time.sleep(5)
  366.  
  367. if __name__ == "__main__":
  368. main()
  369.  
Advertisement
Add Comment
Please, Sign In to add comment