Add 6 Polymarket trading skills with paper trading engine
Composable Agent Skills (SKILL.md format) for Polymarket prediction market trading. Includes scanner, analyzer, monitor, paper trader, strategy advisor, and live executor. All tested against live Polymarket APIs. Security audited with all HIGH/MEDIUM findings resolved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6a03bbe2e5
commit
068b2adc75
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check Polymarket positions, balances, and trade history.
|
||||
|
||||
Requires environment variable:
|
||||
POLYMARKET_PRIVATE_KEY - Wallet private key for authenticated access
|
||||
|
||||
Displays: wallet address, USDC balance, open orders, recent trades.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from py_clob_client.client import ClobClient
|
||||
from py_clob_client.clob_types import (
|
||||
ApiCreds,
|
||||
BalanceAllowanceParams,
|
||||
OpenOrderParams,
|
||||
TradeParams,
|
||||
)
|
||||
|
||||
|
||||
CLOB_HOST = "https://clob.polymarket.com"
|
||||
CHAIN_ID = 137
|
||||
LOG_FILE = Path.home() / ".polymarket-live" / "trades.log"
|
||||
|
||||
|
||||
def create_authenticated_client() -> ClobClient:
|
||||
"""Create an L2-authenticated ClobClient."""
|
||||
key = os.environ.get("POLYMARKET_PRIVATE_KEY", "")
|
||||
if not key:
|
||||
print(
|
||||
"POLYMARKET_PRIVATE_KEY not set.\n"
|
||||
"Set it to your wallet private key to view positions.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
client = ClobClient(CLOB_HOST, chain_id=CHAIN_ID, key=key)
|
||||
creds = client.create_or_derive_api_creds()
|
||||
client.set_api_creds(creds)
|
||||
return client
|
||||
|
||||
|
||||
def show_balance(client: ClobClient):
|
||||
"""Display USDC balance and allowance."""
|
||||
address = client.get_address()
|
||||
print(f"Wallet Address: {address}")
|
||||
print()
|
||||
|
||||
try:
|
||||
params = BalanceAllowanceParams(asset_type="COLLATERAL")
|
||||
result = client.get_balance_allowance(params)
|
||||
print("USDC Balance & Allowance:")
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
except Exception as e:
|
||||
print(f"Error fetching balance: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def show_orders(client: ClobClient):
|
||||
"""Display open orders."""
|
||||
print("Open Orders:")
|
||||
print("-" * 70)
|
||||
|
||||
try:
|
||||
orders = client.get_orders()
|
||||
if not orders:
|
||||
print(" No open orders.")
|
||||
return
|
||||
|
||||
for order in orders:
|
||||
print(json.dumps(order, indent=2, default=str))
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error fetching orders: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def show_trades(client: ClobClient):
|
||||
"""Display recent trade history."""
|
||||
print("Recent Trades:")
|
||||
print("-" * 70)
|
||||
|
||||
try:
|
||||
trades = client.get_trades()
|
||||
if not trades:
|
||||
print(" No trades found.")
|
||||
return
|
||||
|
||||
for trade in trades[:20]: # Show last 20
|
||||
print(json.dumps(trade, indent=2, default=str))
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error fetching trades: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def show_local_log():
|
||||
"""Display local trade log."""
|
||||
print("Local Trade Log (~/.polymarket-live/trades.log):")
|
||||
print("-" * 70)
|
||||
|
||||
if not LOG_FILE.exists():
|
||||
print(" No local trade log found.")
|
||||
return
|
||||
|
||||
lines = LOG_FILE.read_text().splitlines()
|
||||
if not lines:
|
||||
print(" Trade log is empty.")
|
||||
return
|
||||
|
||||
# Show last 20 entries
|
||||
for line in lines[-20:]:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
ts = entry.get("timestamp", "?")[:19]
|
||||
status = entry.get("status", "?")
|
||||
side = entry.get("side", "?")
|
||||
token = entry.get("token_id", "?")[:20]
|
||||
cost = entry.get("cost_usd", "?")
|
||||
print(f" {ts} {status:<10} {side:<5} ${cost} {token}...")
|
||||
except json.JSONDecodeError:
|
||||
print(f" {line[:80]}")
|
||||
|
||||
# Show today's summary
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
daily_total = 0.0
|
||||
daily_count = 0
|
||||
for line in lines:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
if entry.get("timestamp", "").startswith(today) and entry.get("status") == "EXECUTED":
|
||||
daily_total += float(entry.get("cost_usd", 0))
|
||||
daily_count += 1
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
|
||||
print()
|
||||
print(f" Today ({today}): {daily_count} trades, ${daily_total:.2f} total spent")
|
||||
|
||||
|
||||
def show_summary(client: ClobClient):
|
||||
"""Display a comprehensive summary."""
|
||||
address = client.get_address()
|
||||
print(f"Polymarket Position Summary")
|
||||
print(f"Wallet: {address}")
|
||||
print("=" * 60)
|
||||
|
||||
# Balance
|
||||
print()
|
||||
show_balance(client)
|
||||
|
||||
# Open orders
|
||||
print()
|
||||
show_orders(client)
|
||||
|
||||
# Local log summary
|
||||
print()
|
||||
show_local_log()
|
||||
|
||||
# Safety status
|
||||
print()
|
||||
print("Safety Status:")
|
||||
confirm = os.environ.get("POLYMARKET_CONFIRM", "")
|
||||
max_size = os.environ.get("POLYMARKET_MAX_SIZE", "10")
|
||||
daily_limit = os.environ.get("POLYMARKET_DAILY_LOSS_LIMIT", "50")
|
||||
print(f" POLYMARKET_CONFIRM: {'ENABLED' if confirm == 'true' else 'DISABLED (trades blocked)'}")
|
||||
print(f" POLYMARKET_MAX_SIZE: ${max_size}")
|
||||
print(f" POLYMARKET_DAILY_LOSS_LIMIT: ${daily_limit}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Check Polymarket positions and balances"
|
||||
)
|
||||
parser.add_argument("--balance", action="store_true", help="Show USDC balance only")
|
||||
parser.add_argument("--orders", action="store_true", help="Show open orders only")
|
||||
parser.add_argument("--trades", action="store_true", help="Show trade history only")
|
||||
parser.add_argument("--log", action="store_true", help="Show local trade log only")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Local log doesn't need authentication
|
||||
if args.log:
|
||||
show_local_log()
|
||||
return
|
||||
|
||||
client = create_authenticated_client()
|
||||
|
||||
if args.balance:
|
||||
show_balance(client)
|
||||
elif args.orders:
|
||||
show_orders(client)
|
||||
elif args.trades:
|
||||
show_trades(client)
|
||||
else:
|
||||
show_summary(client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute live trades on Polymarket with mandatory human confirmation.
|
||||
|
||||
SAFETY: Every trade requires interactive confirmation. No autonomous execution.
|
||||
|
||||
Requires environment variables:
|
||||
POLYMARKET_PRIVATE_KEY - Burner wallet private key (NEVER main wallet)
|
||||
POLYMARKET_CONFIRM=true - Safety gate (must be exactly "true")
|
||||
|
||||
Optional environment variables:
|
||||
POLYMARKET_MAX_SIZE - Max $ per trade (default: 10)
|
||||
POLYMARKET_DAILY_LOSS_LIMIT - Max daily loss in $ (default: 50)
|
||||
|
||||
All trades are logged to ~/.polymarket-live/trades.log
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from py_clob_client.client import ClobClient
|
||||
from py_clob_client.clob_types import (
|
||||
ApiCreds,
|
||||
OrderArgs,
|
||||
MarketOrderArgs,
|
||||
OrderType,
|
||||
)
|
||||
|
||||
|
||||
CLOB_HOST = "https://clob.polymarket.com"
|
||||
CHAIN_ID = 137 # Polygon mainnet
|
||||
LOG_DIR = Path.home() / ".polymarket-live"
|
||||
LOG_FILE = LOG_DIR / "trades.log"
|
||||
|
||||
|
||||
def check_safety_gates() -> tuple[bool, str]:
|
||||
"""Verify all safety gates are in place. Returns (ok, message)."""
|
||||
key = os.environ.get("POLYMARKET_PRIVATE_KEY", "")
|
||||
if not key:
|
||||
return False, (
|
||||
"POLYMARKET_PRIVATE_KEY not set.\n"
|
||||
"Set it to your BURNER wallet private key (never your main wallet).\n"
|
||||
"See references/security.md for setup instructions."
|
||||
)
|
||||
|
||||
confirm = os.environ.get("POLYMARKET_CONFIRM", "")
|
||||
if confirm != "true":
|
||||
return False, (
|
||||
"POLYMARKET_CONFIRM is not set to 'true'.\n"
|
||||
"This safety gate prevents accidental trade execution.\n"
|
||||
"Set POLYMARKET_CONFIRM=true when you are ready for live trading."
|
||||
)
|
||||
|
||||
return True, "OK"
|
||||
|
||||
|
||||
def get_max_size() -> float:
|
||||
"""Get maximum position size from env or default."""
|
||||
try:
|
||||
return float(os.environ.get("POLYMARKET_MAX_SIZE", "10"))
|
||||
except ValueError:
|
||||
return 10.0
|
||||
|
||||
|
||||
def get_daily_loss_limit() -> float:
|
||||
"""Get daily loss limit from env or default."""
|
||||
try:
|
||||
return float(os.environ.get("POLYMARKET_DAILY_LOSS_LIMIT", "50"))
|
||||
except ValueError:
|
||||
return 50.0
|
||||
|
||||
|
||||
def get_daily_spending() -> float:
|
||||
"""Calculate total spending today from the trade log."""
|
||||
if not LOG_FILE.exists():
|
||||
return 0.0
|
||||
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
total = 0.0
|
||||
|
||||
for line in LOG_FILE.read_text().splitlines():
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
if entry.get("timestamp", "").startswith(today) and entry.get("status") == "EXECUTED":
|
||||
total += float(entry.get("cost_usd", 0))
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def log_trade(entry: dict):
|
||||
"""Append a trade entry to the log file."""
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
||||
|
||||
def create_authenticated_client() -> ClobClient:
|
||||
"""Create an L2-authenticated ClobClient."""
|
||||
key = os.environ["POLYMARKET_PRIVATE_KEY"]
|
||||
|
||||
# Initialize L1 client
|
||||
client = ClobClient(CLOB_HOST, chain_id=CHAIN_ID, key=key)
|
||||
|
||||
# Derive L2 API credentials
|
||||
creds = client.create_or_derive_api_creds()
|
||||
client.set_api_creds(creds)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def get_orderbook_context(client: ClobClient, token_id: str) -> dict:
|
||||
"""Fetch order book context for display."""
|
||||
try:
|
||||
book = client.get_order_book(token_id)
|
||||
bids = [(float(b.price), float(b.size)) for b in (book.bids or [])]
|
||||
asks = [(float(a.price), float(a.size)) for a in (book.asks or [])]
|
||||
bids.sort(key=lambda x: x[0], reverse=True)
|
||||
asks.sort(key=lambda x: x[0])
|
||||
|
||||
return {
|
||||
"best_bid": bids[0] if bids else None,
|
||||
"best_ask": asks[0] if asks else None,
|
||||
"bid_depth_5": sum(s for _, s in bids[:5]),
|
||||
"ask_depth_5": sum(s for _, s in asks[:5]),
|
||||
"spread": (asks[0][0] - bids[0][0]) if (bids and asks) else None,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def display_trade_confirmation(
|
||||
side: str,
|
||||
token_id: str,
|
||||
size: float | None,
|
||||
amount: float | None,
|
||||
price: float | None,
|
||||
is_market: bool,
|
||||
context: dict,
|
||||
max_size: float,
|
||||
daily_spent: float,
|
||||
daily_limit: float,
|
||||
) -> str:
|
||||
"""Display full trade details and return confirmation prompt text."""
|
||||
lines = []
|
||||
lines.append("")
|
||||
lines.append("=" * 60)
|
||||
lines.append(" LIVE TRADE CONFIRMATION REQUIRED")
|
||||
lines.append("=" * 60)
|
||||
lines.append("")
|
||||
lines.append(f" Side: {side}")
|
||||
lines.append(f" Token ID: {token_id[:40]}...")
|
||||
|
||||
if is_market:
|
||||
lines.append(f" Order Type: MARKET (Fill-or-Kill)")
|
||||
lines.append(f" Amount: ${amount:.2f} USD")
|
||||
else:
|
||||
lines.append(f" Order Type: LIMIT (Good-til-Cancelled)")
|
||||
lines.append(f" Size: {size:.2f} shares")
|
||||
lines.append(f" Price: ${price:.4f} per share")
|
||||
est_cost = size * price
|
||||
lines.append(f" Est. Cost: ${est_cost:.2f} USD")
|
||||
|
||||
lines.append("")
|
||||
lines.append(" Order Book Context:")
|
||||
if "error" in context:
|
||||
lines.append(f" ERROR: {context['error']}")
|
||||
else:
|
||||
bb = context.get("best_bid")
|
||||
ba = context.get("best_ask")
|
||||
lines.append(f" Best Bid: ${bb[0]:.4f} ({bb[1]:.0f} shares)" if bb else " Best Bid: N/A")
|
||||
lines.append(f" Best Ask: ${ba[0]:.4f} ({ba[1]:.0f} shares)" if ba else " Best Ask: N/A")
|
||||
spread = context.get("spread")
|
||||
lines.append(f" Spread: ${spread:.4f}" if spread is not None else " Spread: N/A")
|
||||
lines.append(f" Bid Depth (5 lvls): {context.get('bid_depth_5', 0):,.0f} shares")
|
||||
lines.append(f" Ask Depth (5 lvls): {context.get('ask_depth_5', 0):,.0f} shares")
|
||||
|
||||
lines.append("")
|
||||
lines.append(" Risk Controls:")
|
||||
lines.append(f" Max trade size: ${max_size:.2f}")
|
||||
lines.append(f" Daily spent: ${daily_spent:.2f} / ${daily_limit:.2f}")
|
||||
|
||||
remaining = daily_limit - daily_spent
|
||||
trade_cost = amount if is_market else (size * price if size and price else 0)
|
||||
if trade_cost > remaining:
|
||||
lines.append(f" WARNING: Trade (${trade_cost:.2f}) exceeds remaining daily budget (${remaining:.2f})")
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 60)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def execute_limit_order(
|
||||
client: ClobClient,
|
||||
token_id: str,
|
||||
side: str,
|
||||
size: float,
|
||||
price: float,
|
||||
) -> dict:
|
||||
"""Create and post a limit order."""
|
||||
order_args = OrderArgs(
|
||||
token_id=token_id,
|
||||
price=price,
|
||||
size=size,
|
||||
side=side,
|
||||
)
|
||||
signed_order = client.create_order(order_args)
|
||||
result = client.post_order(signed_order, orderType=OrderType.GTC)
|
||||
return result
|
||||
|
||||
|
||||
def execute_market_order(
|
||||
client: ClobClient,
|
||||
token_id: str,
|
||||
side: str,
|
||||
amount: float,
|
||||
) -> dict:
|
||||
"""Create and post a market order."""
|
||||
order_args = MarketOrderArgs(
|
||||
token_id=token_id,
|
||||
amount=amount,
|
||||
side=side,
|
||||
)
|
||||
signed_order = client.create_market_order(order_args)
|
||||
result = client.post_order(signed_order, orderType=OrderType.FOK)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Execute a live trade on Polymarket (requires confirmation)"
|
||||
)
|
||||
parser.add_argument("--token-id", required=True, help="CLOB token ID")
|
||||
parser.add_argument(
|
||||
"--side", required=True, choices=["BUY", "SELL"],
|
||||
help="Trade side: BUY or SELL"
|
||||
)
|
||||
parser.add_argument("--size", type=float, help="Number of shares (for limit orders)")
|
||||
parser.add_argument("--price", type=float, help="Limit price per share")
|
||||
parser.add_argument("--amount", type=float, help="USD amount (for market orders)")
|
||||
parser.add_argument("--market", action="store_true", help="Market order (FOK)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if args.market:
|
||||
if not args.amount:
|
||||
print("ERROR: --amount required for market orders", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
if not args.size or not args.price:
|
||||
print("ERROR: --size and --price required for limit orders", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check safety gates
|
||||
ok, msg = check_safety_gates()
|
||||
if not ok:
|
||||
print(f"SAFETY GATE FAILED:\n{msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check position size limits
|
||||
max_size = get_max_size()
|
||||
trade_cost = args.amount if args.market else (args.size * args.price)
|
||||
|
||||
if trade_cost > max_size:
|
||||
print(
|
||||
f"BLOCKED: Trade cost ${trade_cost:.2f} exceeds max size ${max_size:.2f}.\n"
|
||||
f"Increase POLYMARKET_MAX_SIZE if intentional.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Check daily loss limit
|
||||
daily_limit = get_daily_loss_limit()
|
||||
daily_spent = get_daily_spending()
|
||||
if daily_spent + trade_cost > daily_limit:
|
||||
print(
|
||||
f"BLOCKED: Daily spending ${daily_spent:.2f} + this trade ${trade_cost:.2f} "
|
||||
f"= ${daily_spent + trade_cost:.2f} exceeds daily limit ${daily_limit:.2f}.\n"
|
||||
f"Increase POLYMARKET_DAILY_LOSS_LIMIT or wait until tomorrow.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Create authenticated client
|
||||
try:
|
||||
client = create_authenticated_client()
|
||||
except Exception as e:
|
||||
print(f"ERROR creating authenticated client: {e}", file=sys.stderr)
|
||||
print("Check POLYMARKET_PRIVATE_KEY and network connectivity.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get order book context
|
||||
context = get_orderbook_context(client, args.token_id)
|
||||
|
||||
# Display confirmation
|
||||
confirmation = display_trade_confirmation(
|
||||
side=args.side,
|
||||
token_id=args.token_id,
|
||||
size=args.size,
|
||||
amount=args.amount,
|
||||
price=args.price,
|
||||
is_market=args.market,
|
||||
context=context,
|
||||
max_size=max_size,
|
||||
daily_spent=daily_spent,
|
||||
daily_limit=daily_limit,
|
||||
)
|
||||
print(confirmation)
|
||||
|
||||
# Ask for confirmation
|
||||
try:
|
||||
response = input("\n Type 'yes' to execute this trade: ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nTrade cancelled.")
|
||||
log_trade({
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "CANCELLED",
|
||||
"reason": "User did not confirm",
|
||||
"token_id": args.token_id,
|
||||
"side": args.side,
|
||||
"size": args.size,
|
||||
"price": args.price,
|
||||
"amount": args.amount,
|
||||
"is_market": args.market,
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
if response != "yes":
|
||||
print("Trade cancelled. You must type exactly 'yes' to confirm.")
|
||||
log_trade({
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "CANCELLED",
|
||||
"reason": f"User typed: {response!r}",
|
||||
"token_id": args.token_id,
|
||||
"side": args.side,
|
||||
"size": args.size,
|
||||
"price": args.price,
|
||||
"amount": args.amount,
|
||||
"is_market": args.market,
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
# Execute the trade
|
||||
print("\nExecuting trade...")
|
||||
try:
|
||||
if args.market:
|
||||
result = execute_market_order(client, args.token_id, args.side, args.amount)
|
||||
else:
|
||||
result = execute_limit_order(client, args.token_id, args.side, args.size, args.price)
|
||||
|
||||
print(f"\nTrade submitted successfully!")
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
|
||||
log_trade({
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "EXECUTED",
|
||||
"token_id": args.token_id,
|
||||
"side": args.side,
|
||||
"size": args.size,
|
||||
"price": args.price,
|
||||
"amount": args.amount,
|
||||
"is_market": args.market,
|
||||
"cost_usd": trade_cost,
|
||||
"result": result,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nTrade FAILED: {e}", file=sys.stderr)
|
||||
log_trade({
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "FAILED",
|
||||
"error": str(e),
|
||||
"token_id": args.token_id,
|
||||
"side": args.side,
|
||||
"size": args.size,
|
||||
"price": args.price,
|
||||
"amount": args.amount,
|
||||
"is_market": args.market,
|
||||
"cost_usd": trade_cost,
|
||||
})
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user