"""CLOB order executor โ places limit orders on Polymarket"""
import time
from loguru import logger
from config import PRIVATE_KEY, PROXY_ADDRESS, SIGNATURE_TYPE, CHAIN_ID, CLOB_API_URL, DRY_RUN, TRADE_PROXY
import db
MAX_RETRIES = 3
RETRY_BACKOFF = [1, 3, 7] # seconds between retries
# Lazy init โ only connect when needed
_client = None
_proxy_applied = False
def _apply_proxy():
"""
Configure CLOB HTTP client to use proxy.
py-clob-client uses a module-level httpx.Client without proxy support,
so we replace it with a proxy-enabled instance. This is verified after
patching to catch breaking changes in the library.
"""
global _proxy_applied
if _proxy_applied or not TRADE_PROXY:
return
try:
import httpx
from py_clob_client.http_helpers import helpers
if not hasattr(helpers, '_http_client'):
logger.error("py_clob_client internal API changed: helpers._http_client not found. "
"Orders will be sent WITHOUT proxy โ IP may be exposed!")
return
helpers._http_client = httpx.Client(http2=True, proxy=TRADE_PROXY, timeout=30)
# Verify patch took effect
patched = getattr(helpers, '_http_client', None)
if patched is None or not hasattr(patched, '_transport'):
logger.error("Proxy patch verification failed โ proxy may not be active!")
return
_proxy_applied = True
_proxy_host = TRADE_PROXY.split('@')[-1] if '@' in TRADE_PROXY else TRADE_PROXY
logger.info(f"CLOB proxy configured: {_proxy_host}")
except Exception as e:
logger.error(f"Failed to configure trade proxy: {e}. Orders will be sent WITHOUT proxy!")
def get_client():
"""Lazy init py-clob-client with proxy"""
global _client
if _client is not None:
return _client
if not PRIVATE_KEY or PRIVATE_KEY == "your_polygon_wallet_private_key_here":
logger.warning("No PRIVATE_KEY configured โ executor disabled")
return None
# Apply proxy BEFORE creating client (client init may make HTTP calls)
_apply_proxy()
try:
from py_clob_client.client import ClobClient
kwargs = {
"host": CLOB_API_URL,
"key": PRIVATE_KEY,
"chain_id": CHAIN_ID,
"signature_type": SIGNATURE_TYPE,
}
if PROXY_ADDRESS and SIGNATURE_TYPE == 2:
kwargs["funder"] = PROXY_ADDRESS
_client = ClobClient(**kwargs)
_client.set_api_creds(_client.create_or_derive_api_creds())
logger.info("CLOB client initialized")
return _client
except Exception as e:
logger.error(f"CLOB client init failed: {e}")
return None
def place_bet(signal: dict, dry_run: bool = None) -> dict:
"""
Place a limit order on Polymarket.
signal: {market_id, side, token_id, price, size, edge, ...}
dry_run: override config DRY_RUN
Returns: {success, order_id, message, dry_run}
"""
if dry_run is None:
dry_run = DRY_RUN
result = {
"success": False,
"order_id": None,
"message": "",
"dry_run": dry_run,
}
# === Idempotency guard: skip if active trade already exists for this market+side ===
active_trades = db.get_active_trades()
for t in active_trades:
if t["market_id"] == signal["market_id"] and t["side"] == signal["side"]:
result["message"] = f"Duplicate blocked: active trade #{t['id']} already exists for {signal['market_id'][:8]}โฆ/{signal['side']}"
logger.warning(f"โ {result['message']}")
return result
if dry_run:
# Simulate order
result["success"] = True
result["order_id"] = f"dry-{signal['market_id'][:8]}-{int(time.time())}"
result["message"] = f"DRY RUN: Would {signal['side']} @ ${signal['price']:.2f} x ${signal['size']}"
logger.info(f"๐งช {result['message']}")
# Save trade to DB
db.save_trade({
"market_id": signal["market_id"],
"order_id": result["order_id"],
"side": signal["side"],
"token_id": signal["token_id"],
"price": signal["price"],
"size": signal["size"],
"edge": signal["edge"],
"edge_tier": signal.get("edge_tier"),
"model_prob": signal["model_prob"],
"market_prob": signal["market_prob"],
"status": "simulated",
"dry_run": 1,
})
return result
# Real order
client = get_client()
if not client:
result["message"] = "CLOB client not available"
logger.error(result["message"])
return result
try:
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY
# signal["size"] is dollar amount, OrderArgs size is number of shares
price = round(signal["price"], 2)
shares = round(signal["size"] / price, 2) if price > 0 else 0
# Polymarket minimum ~5 shares, but cap dollar cost to signal size
MIN_SHARES = 5
if shares < MIN_SHARES:
min_cost = MIN_SHARES * price
if min_cost > signal["size"] * 1.5:
# Minimum order too expensive relative to intended size โ skip
result["message"] = f"Skipped: min {MIN_SHARES} shares @ ${price:.2f} = ${min_cost:.2f} exceeds budget ${signal['size']:.2f}"
logger.warning(f"โ ๏ธ {result['message']}")
return result
shares = MIN_SHARES
order_args = OrderArgs(
token_id=signal["token_id"],
price=price,
size=shares,
side=BUY,
)
signed = client.create_order(order_args)
# === Retry loop for post_order (network-sensitive) ===
last_error = None
for attempt in range(MAX_RETRIES):
try:
response = client.post_order(signed, OrderType.GTC)
order_id = response.get("orderID") or response.get("id", "unknown")
result["success"] = True
result["order_id"] = order_id
result["message"] = f"Order placed: {signal['side']} @ ${signal['price']:.2f} x ${signal['size']}"
logger.info(f"๐ฐ REAL ORDER: {result['message']} (ID: {order_id}, attempt: {attempt+1})")
# Save trade to DB
db.save_trade({
"market_id": signal["market_id"],
"order_id": order_id,
"side": signal["side"],
"token_id": signal["token_id"],
"price": signal["price"],
"size": signal["size"],
"edge": signal["edge"],
"edge_tier": signal.get("edge_tier"),
"model_prob": signal["model_prob"],
"market_prob": signal["market_prob"],
"status": "pending",
"dry_run": 0,
})
return result
except Exception as e:
last_error = e
if attempt < MAX_RETRIES - 1:
wait = RETRY_BACKOFF[attempt]
logger.warning(f"โ ๏ธ post_order attempt {attempt+1} failed: {e} โ retrying in {wait}s")
time.sleep(wait)
# All retries exhausted โ save as unverified so we can check later
result["message"] = f"Order failed after {MAX_RETRIES} attempts: {last_error}"
logger.error(f"โ {result['message']}")
db.save_trade({
"market_id": signal["market_id"],
"order_id": None,
"side": signal["side"],
"token_id": signal["token_id"],
"price": signal["price"],
"size": signal["size"],
"edge": signal["edge"],
"edge_tier": signal.get("edge_tier"),
"model_prob": signal["model_prob"],
"market_prob": signal["market_prob"],
"status": "unverified", # may or may not have been placed on exchange
"dry_run": 0,
})
return result
except Exception as e:
result["message"] = f"Order creation failed: {e}"
logger.error(result["message"])
return result
def cancel_order(order_id: str) -> bool:
"""Cancel an open order"""
client = get_client()
if not client:
return False
try:
client.cancel(order_id)
logger.info(f"Cancelled order {order_id}")
return True
except Exception as e:
logger.error(f"Cancel failed: {e}")
return False
def redeem_position(condition_id: str, neg_risk: bool = True) -> dict:
"""
Redeem a resolved position on Polymarket.
Burns winning tokens โ returns USDC to proxy wallet.
Uses polymarket-apis package which supports Safe/Gnosis proxy wallets.
"""
result = {"success": False, "message": "", "tx_hash": None}
if DRY_RUN:
result["success"] = True
result["message"] = f"DRY RUN: Would redeem {condition_id[:16]}..."
logger.info(f"๐งช {result['message']}")
return result
try:
from polymarket_apis import PolymarketWeb3Client
web3_client = PolymarketWeb3Client(
private_key=PRIVATE_KEY,
signature_type=SIGNATURE_TYPE,
)
# Redeem both outcomes (winning = $1/share, losing = $0)
# amounts=[0,0] โ redeems ALL available tokens for both outcomes
receipt = web3_client.redeem_position(
condition_id=condition_id,
amounts=[0, 0],
neg_risk=neg_risk,
)
tx_hash = receipt.get("transactionHash", "") if isinstance(receipt, dict) else str(receipt)
result["success"] = True
result["tx_hash"] = tx_hash
result["message"] = f"Redeemed {condition_id[:16]}... โ tx: {str(tx_hash)[:16]}..."
logger.info(f"๐ฐ REDEEM: {result['message']}")
except Exception as e:
result["message"] = f"Redeem failed for {condition_id[:16]}...: {e}"
logger.error(f"โ {result['message']}")
return result
def get_balance() -> float | None:
"""Get USDC balance from Polymarket in dollars"""
client = get_client()
if not client:
return None
try:
balance = client.get_balance_allowance()
raw = float(balance.get("balance", 0)) if balance else 0
# Polymarket returns balance in USDC micro-units (6 decimals)
return raw / 1_000_000 if raw > 1000 else raw
except Exception as e:
logger.error(f"Balance check failed: {e}")
return None
๐ Git History
8734f64fix: weather-bot resolver + borderline filter + auto-redeem8 days ago
592e45efix: comprehensive weather bot audit โ 20 fixes across all modules9 days ago
Show last diff
Loading...