โ† Back
โ˜†
"""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...