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
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: polymarket-live-executor
|
||||
description: >
|
||||
Use this skill when the user wants to execute a real trade on Polymarket, place a live
|
||||
order, go live, buy or sell on Polymarket, check real positions, or manage a live trading
|
||||
wallet. Triggers: "execute trade", "live trade", "real trade", "go live",
|
||||
"place order polymarket", "buy on polymarket", "sell on polymarket", "check positions",
|
||||
"my balance", "cancel order", "live portfolio".
|
||||
CRITICAL: This skill executes REAL trades with REAL money. Every trade requires explicit
|
||||
human confirmation before execution. Never execute autonomously.
|
||||
---
|
||||
|
||||
# Polymarket Live Executor
|
||||
|
||||
Execute real trades on Polymarket with mandatory human-in-the-loop confirmation.
|
||||
This skill requires L2 authentication (wallet private key) and enforces strict
|
||||
safety controls on every operation.
|
||||
|
||||
## SAFETY REQUIREMENTS
|
||||
|
||||
**Every trade requires explicit user confirmation.** The agent must:
|
||||
|
||||
1. Display full trade details (market, side, size, price, estimated cost)
|
||||
2. Show current order book context (spread, depth at target price)
|
||||
3. Wait for the user to type "yes" or "confirm" before proceeding
|
||||
4. Never batch or auto-confirm trades
|
||||
|
||||
**Environment safeguards** (enforced by all scripts):
|
||||
- `POLYMARKET_PRIVATE_KEY` must be set (burner wallet only -- NEVER a main wallet)
|
||||
- `POLYMARKET_CONFIRM=true` must be set to enable any trade execution
|
||||
- Position size hard-capped (default $10, configurable via `POLYMARKET_MAX_SIZE`)
|
||||
- Daily loss limit tracked in `~/.polymarket-live/trades.log`
|
||||
|
||||
## Setup
|
||||
|
||||
Before using this skill, the user must:
|
||||
|
||||
1. Create a burner wallet (see `references/security.md`)
|
||||
2. Fund it with a small amount of USDC on Polygon
|
||||
3. Set environment variables:
|
||||
```bash
|
||||
export POLYMARKET_PRIVATE_KEY="0x..." # Burner wallet only!
|
||||
export POLYMARKET_CONFIRM=true # Safety gate
|
||||
export POLYMARKET_MAX_SIZE=10 # Max $ per trade (default: 10)
|
||||
export POLYMARKET_DAILY_LOSS_LIMIT=50 # Max daily loss (default: 50)
|
||||
```
|
||||
4. Review the `references/live-trading-checklist.md` before any live trade
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 1. Execute Trade (`scripts/execute_live.py`)
|
||||
|
||||
Place a real order on Polymarket.
|
||||
|
||||
```bash
|
||||
# Limit order: buy 5 YES shares at $0.60
|
||||
python scripts/execute_live.py --token-id <ID> --side BUY --size 5 --price 0.60
|
||||
|
||||
# Market order: buy $5 worth at market price
|
||||
python scripts/execute_live.py --token-id <ID> --side BUY --amount 5 --market
|
||||
|
||||
# Sell: sell 10 shares at $0.75
|
||||
python scripts/execute_live.py --token-id <ID> --side SELL --size 10 --price 0.75
|
||||
```
|
||||
|
||||
The script will display full trade details and require interactive confirmation.
|
||||
|
||||
### 2. Check Positions (`scripts/check_positions.py`)
|
||||
|
||||
View wallet balance, open orders, and trade history.
|
||||
|
||||
```bash
|
||||
python scripts/check_positions.py # Summary
|
||||
python scripts/check_positions.py --orders # Open orders
|
||||
python scripts/check_positions.py --trades # Recent trades
|
||||
python scripts/check_positions.py --balance # USDC balance
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Run analysis with polymarket-analyzer scripts to find opportunities
|
||||
2. Paper-trade the idea with polymarket-paper-trader first
|
||||
3. Review `references/live-trading-checklist.md`
|
||||
4. Set up environment variables and burner wallet
|
||||
5. Use `check_positions.py` to verify wallet state
|
||||
6. Execute trade with `execute_live.py` -- confirm when prompted
|
||||
7. Monitor position with `check_positions.py`
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- All trades logged to `~/.polymarket-live/trades.log`
|
||||
- Daily P&L tracked and enforced against loss limit
|
||||
- Position size caps prevent oversized trades
|
||||
- No autonomous execution -- every trade needs human approval
|
||||
|
||||
## Important Disclaimers
|
||||
|
||||
- This skill executes REAL trades with REAL money
|
||||
- Use only with burner wallets funded with money you can afford to lose
|
||||
- Past analysis does not guarantee future results
|
||||
- Not financial advice -- you are solely responsible for your trades
|
||||
@@ -0,0 +1,68 @@
|
||||
# Pre-Flight Checklist for Live Trading
|
||||
|
||||
Complete ALL items before executing your first live trade.
|
||||
|
||||
## Wallet Setup
|
||||
|
||||
- [ ] Created a dedicated burner wallet (NOT your main wallet)
|
||||
- [ ] Funded with an amount you can afford to lose entirely
|
||||
- [ ] Verified wallet has USDC on Polygon network
|
||||
- [ ] Verified wallet has small MATIC balance for gas
|
||||
- [ ] Private key stored in environment variable only (not in files or code)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
- [ ] `POLYMARKET_PRIVATE_KEY` is set to burner wallet key
|
||||
- [ ] `POLYMARKET_CONFIRM=true` is set (required safety gate)
|
||||
- [ ] `POLYMARKET_MAX_SIZE` is set to your per-trade limit (default: $10)
|
||||
- [ ] `POLYMARKET_DAILY_LOSS_LIMIT` is set (default: $50)
|
||||
- [ ] Verified configuration with `check_positions.py --balance`
|
||||
|
||||
## Analysis Completed
|
||||
|
||||
- [ ] Identified specific market and opportunity using polymarket-analyzer
|
||||
- [ ] Reviewed order book depth with `analyze_orderbook.py`
|
||||
- [ ] Confirmed sufficient liquidity at target price
|
||||
- [ ] Understood the market's resolution criteria
|
||||
- [ ] Checked market end date (not expiring imminently)
|
||||
|
||||
## Paper Trading Validation
|
||||
|
||||
- [ ] Tested the same strategy in paper trading mode first
|
||||
- [ ] Paper trading results reviewed and acceptable
|
||||
- [ ] Understand expected win rate and risk/reward ratio
|
||||
- [ ] Strategy has shown positive expectancy in paper trades
|
||||
|
||||
## Risk Management
|
||||
|
||||
- [ ] Set maximum position size per trade
|
||||
- [ ] Set daily loss limit
|
||||
- [ ] Decided on exit strategy (when to take profit, when to cut loss)
|
||||
- [ ] Understand that prediction markets can go to 0 or 1
|
||||
- [ ] Accepted that all funds in burner wallet could be lost
|
||||
- [ ] Not trading with money needed for bills, rent, or essentials
|
||||
|
||||
## Execution Plan
|
||||
|
||||
- [ ] Know the exact token ID for the trade
|
||||
- [ ] Know the side (BUY or SELL)
|
||||
- [ ] Know the size (number of shares or dollar amount)
|
||||
- [ ] Know the price (limit) or willing to accept market price
|
||||
- [ ] Reviewed current bid-ask spread
|
||||
- [ ] Will carefully review the confirmation prompt before approving
|
||||
|
||||
## After the Trade
|
||||
|
||||
- [ ] Verified trade executed at expected price (check trades.log)
|
||||
- [ ] Set a reminder to check position before market resolution
|
||||
- [ ] Know how to cancel open limit orders if needed
|
||||
- [ ] Plan for monitoring: will check at least daily
|
||||
|
||||
## Emergency Procedures
|
||||
|
||||
If something goes wrong:
|
||||
1. Unset `POLYMARKET_CONFIRM` to prevent any further trades
|
||||
2. Use `check_positions.py --orders` to see open orders
|
||||
3. Cancel all open orders if needed
|
||||
4. Transfer remaining funds out of burner wallet if compromised
|
||||
5. Create a new burner wallet if the old one may be exposed
|
||||
@@ -0,0 +1,123 @@
|
||||
# Security Guide for Live Polymarket Trading
|
||||
|
||||
## Rule #1: NEVER Use Your Main Wallet
|
||||
|
||||
Always create a dedicated **burner wallet** for bot trading. This wallet should:
|
||||
- Be a fresh address with no connection to your primary holdings
|
||||
- Hold only the amount you are willing to lose entirely
|
||||
- Never be used for any other purpose
|
||||
|
||||
If your private key is compromised (through env var leaks, log exposure, or
|
||||
prompt injection attacks), only the burner wallet funds are at risk.
|
||||
|
||||
## Creating a Burner Wallet
|
||||
|
||||
### Option A: Using Python
|
||||
|
||||
```python
|
||||
from eth_account import Account
|
||||
acct = Account.create()
|
||||
print(f"Address: {acct.address}")
|
||||
print(f"Private Key: {acct.key.hex()}")
|
||||
# SAVE THESE SECURELY. Fund address with USDC on Polygon.
|
||||
```
|
||||
|
||||
### Option B: Using MetaMask
|
||||
|
||||
1. Create a new MetaMask profile (not just a new account in existing profile)
|
||||
2. Create a new wallet
|
||||
3. Export the private key (Settings > Security > Export Private Key)
|
||||
4. Fund with USDC on Polygon network
|
||||
|
||||
### Option C: Using cast (Foundry)
|
||||
|
||||
```bash
|
||||
cast wallet new
|
||||
# Save the address and private key
|
||||
```
|
||||
|
||||
## Funding the Burner Wallet
|
||||
|
||||
1. Bridge USDC to Polygon (use official Polygon bridge or a reputable DEX)
|
||||
2. Send only your maximum acceptable loss to the burner address
|
||||
3. Keep some MATIC for gas fees (~0.1 MATIC is usually sufficient)
|
||||
|
||||
## Setting Up L2 Authentication
|
||||
|
||||
The Polymarket CLOB API uses three-tier authentication:
|
||||
|
||||
- **L0**: No auth. Read-only market data.
|
||||
- **L1**: Wallet signature. Can create/sign orders.
|
||||
- **L2**: API key + secret + passphrase. Can post orders, manage positions.
|
||||
|
||||
To set up L2:
|
||||
|
||||
```python
|
||||
from py_clob_client.client import ClobClient
|
||||
|
||||
# Initialize with L1 (private key)
|
||||
client = ClobClient(
|
||||
"https://clob.polymarket.com",
|
||||
chain_id=137, # Polygon mainnet
|
||||
key="0xYOUR_PRIVATE_KEY"
|
||||
)
|
||||
|
||||
# Create API credentials (L2)
|
||||
creds = client.create_or_derive_api_creds()
|
||||
print(f"API Key: {creds.api_key}")
|
||||
print(f"API Secret: {creds.api_secret}")
|
||||
print(f"API Passphrase: {creds.api_passphrase}")
|
||||
```
|
||||
|
||||
Store these credentials securely. The execute_live.py script handles this
|
||||
automatically when POLYMARKET_PRIVATE_KEY is set.
|
||||
|
||||
## Private Key Handling Best Practices
|
||||
|
||||
### DO:
|
||||
- Store the private key in an environment variable (`POLYMARKET_PRIVATE_KEY`)
|
||||
- Use a `.env` file with strict permissions (`chmod 600 .env`)
|
||||
- Unset the variable when not actively trading (`unset POLYMARKET_PRIVATE_KEY`)
|
||||
- Rotate burner wallets periodically (create new wallet, transfer remaining funds)
|
||||
|
||||
### DO NOT:
|
||||
- Hardcode private keys in any script or config file
|
||||
- Commit `.env` files to version control
|
||||
- Share private keys in chat, logs, or error reports
|
||||
- Use the same key for testing and production
|
||||
- Give the LLM/agent direct access to your private key in conversation
|
||||
- Store keys in world-readable files
|
||||
|
||||
### Gitignore Template
|
||||
|
||||
Add to your `.gitignore`:
|
||||
```
|
||||
.env
|
||||
.env.*
|
||||
*.key
|
||||
polymarket-live/
|
||||
~/.polymarket-live/
|
||||
```
|
||||
|
||||
## Prompt Injection Defense
|
||||
|
||||
When using AI agents for trading, be aware of prompt injection risks:
|
||||
|
||||
1. **Never paste untrusted content into agent context** when live trading is enabled
|
||||
2. **Market descriptions could contain malicious instructions** -- the agent should
|
||||
never execute trades based on text found in market descriptions
|
||||
3. **The POLYMARKET_CONFIRM=true env var** acts as a safety gate -- without it,
|
||||
no trade can execute regardless of what the agent is told to do
|
||||
4. **Review every trade confirmation** before approving -- the agent must show you
|
||||
the exact parameters before execution
|
||||
|
||||
## Maximum Funding Recommendations
|
||||
|
||||
| Experience Level | Max Wallet Fund | Max Per Trade | Daily Loss Limit |
|
||||
|------------------|-----------------|---------------|------------------|
|
||||
| First time | $25 | $5 | $10 |
|
||||
| Learning | $100 | $10 | $25 |
|
||||
| Experienced | $500 | $50 | $100 |
|
||||
| Advanced | $2,000+ | $200 | $500 |
|
||||
|
||||
Start small. You can always add more funds later.
|
||||
+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