Add health check, backtest, correlation tracker, wallet setup, and skills registry

New scripts:
- health_check.py: one-command session-start workflow (portfolio + live prices +
  drawdown + stop losses → GREEN/YELLOW/RED status)
- backtest.py: performance analysis with live-readiness assessment against
  CLAUDE.md prerequisites (20+ trades, >55% win rate, Sharpe >0.5)
- correlation_tracker.py: detects hidden correlated exposure in portfolio
  (e.g., 3 insider-trading bets = one cluster)
- setup_wallet.py: burner wallet creation, env var verification, on-chain
  balance check for live trading setup

Also adds:
- .env.example template for live trading configuration
- .well-known/skills/index.json for Agent Skills registry discovery
- Updated SKILL.md files documenting new scripts
- .gitignore entries for .env, .polymarket-live/, and key files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Polymarket Skills Builder
2026-02-26 09:05:24 +00:00
parent 81f0c9965f
commit 22fcb4355b
11 changed files with 3248 additions and 11 deletions
+5
View File
@@ -1,3 +1,8 @@
__pycache__/
*.pyc
.polymarket-paper/
.polymarket-live/
.env
.env.*
!.env.example
*.key
+77
View File
@@ -0,0 +1,77 @@
{
"skills": [
{
"name": "polymarket-scanner",
"description": "Use this skill whenever the user wants to browse, search, scan, or explore Polymarket prediction markets. Includes finding markets by topic, checking prices and order books, viewing volumes, and fetching live data.",
"files": [
"SKILL.md",
"references/api-guide.md",
"references/market-types.md",
"scripts/get_orderbook.py",
"scripts/get_prices.py",
"scripts/scan_markets.py"
]
},
{
"name": "polymarket-analyzer",
"description": "Use this skill whenever the user wants to find trading opportunities, detect arbitrage, analyze a market, perform edge detection, find mispricing, do probability analysis, evaluate orderbook depth, or find momentum signals.",
"files": [
"SKILL.md",
"references/fee-model.md",
"references/viable-strategies.md",
"scripts/analyze_orderbook.py",
"scripts/correlation_tracker.py",
"scripts/find_edges.py",
"scripts/momentum_scanner.py"
]
},
{
"name": "polymarket-monitor",
"description": "Use this skill whenever the user wants to monitor, watch, or track Polymarket prediction market prices over time. Includes setting up price alerts, watching for significant price movements, tracking spread changes, and monitoring volume spikes.",
"files": [
"SKILL.md",
"references/monitoring-guide.md",
"scripts/monitor_prices.py",
"scripts/watch_market.py"
]
},
{
"name": "polymarket-paper-trader",
"description": "Use this skill whenever the user wants to paper trade, simulate trades, practice trading, backtest strategies, manage a virtual portfolio, or track simulated P&L on Polymarket prediction markets. This is the core trading engine with zero financial risk.",
"files": [
"SKILL.md",
"references/paper-trading-guide.md",
"references/risk-rules.md",
"scripts/execute_paper.py",
"scripts/health_check.py",
"scripts/paper_engine.py",
"scripts/portfolio_report.py"
]
},
{
"name": "polymarket-strategy-advisor",
"description": "Use this skill whenever the user wants trading strategy advice, trade recommendations, portfolio guidance, or prediction market analysis that leads to actionable trades. Covers position sizing, Kelly criterion, risk management, and daily review.",
"files": [
"SKILL.md",
"references/decision-framework.md",
"references/viable-strategies.md",
"scripts/advisor.py",
"scripts/backtest.py",
"scripts/daily_review.py"
]
},
{
"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. CRITICAL: Executes REAL trades with REAL money requiring explicit human confirmation.",
"files": [
"SKILL.md",
".env.example",
"references/live-trading-checklist.md",
"references/security.md",
"scripts/check_positions.py",
"scripts/execute_live.py",
"scripts/setup_wallet.py"
]
}
]
}
+15
View File
@@ -59,6 +59,21 @@ python scripts/momentum_scanner.py
python scripts/momentum_scanner.py --min-volume 10000 --limit 300
```
### 4. Correlation Tracker (`scripts/correlation_tracker.py`)
Detect hidden correlated exposure in your portfolio:
- Groups positions by topic (crypto, politics, sports, geopolitics, etc.)
- Detects shared qualifiers ("insider trading", "FIFA World Cup", etc.)
- Warns when correlated clusters exceed concentration limits
- Outputs diversification score (0-100)
```bash
python scripts/correlation_tracker.py
python scripts/correlation_tracker.py --json
python scripts/correlation_tracker.py --threshold 0.10
```
## Workflow
1. Run `find_edges.py` to scan for arbitrage across all active markets
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
# Polymarket Live Trading — Environment Configuration
# Copy this file to .env and fill in your values:
# cp .env.example .env && chmod 600 .env
#
# NEVER commit .env to version control.
# NEVER use your main wallet. Create a dedicated burner wallet.
# Required: Burner wallet private key (with 0x prefix)
# Create one with: cast wallet new OR python -c "from eth_account import Account; a=Account.create(); print(f'Address: {a.address}\nKey: {a.key.hex()}')"
POLYMARKET_PRIVATE_KEY=0x_YOUR_BURNER_WALLET_PRIVATE_KEY_HERE
# Required: Safety gate — must be "true" for any trade to execute
POLYMARKET_CONFIRM=true
# Max dollars per trade (start small!)
# First time: $5 | Learning: $10 | Experienced: $50 | Advanced: $200
POLYMARKET_MAX_SIZE=5
# Max daily loss before all trading halts
# First time: $10 | Learning: $25 | Experienced: $100 | Advanced: $500
POLYMARKET_DAILY_LOSS_LIMIT=10
+27 -11
View File
@@ -33,18 +33,34 @@ safety controls on every operation.
## Setup
Before using this skill, the user must:
Use the setup wizard to configure everything:
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
```bash
# Step 1: Create a burner wallet
python scripts/setup_wallet.py --create
# Step 2: Fund wallet with USDC on Polygon (manually via MetaMask/bridge)
# Step 3: Copy and fill in .env
cp .env.example .env && chmod 600 .env
# Edit .env with your private key and limits
# Step 4: Verify everything is configured
python scripts/setup_wallet.py --verify
# Step 5: Check on-chain balance
python scripts/setup_wallet.py --check-balance
```
Or set environment variables manually:
```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)
```
Review the `references/live-trading-checklist.md` before any live trade.
## Available Scripts
@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
Polymarket Live Trading — Wallet Setup & Verification
Creates a burner wallet OR verifies an existing configuration.
Run this BEFORE your first live trade.
Usage:
python setup_wallet.py --create # Generate new burner wallet
python setup_wallet.py --verify # Check existing env vars
python setup_wallet.py --check-balance # Verify on-chain USDC balance
"""
import argparse
import json
import os
import sys
import stat
from pathlib import Path
# Polygon USDC contract
USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
POLYGON_RPC = "https://polygon-rpc.com"
def create_wallet():
"""Generate a new burner wallet and output setup instructions."""
try:
from eth_account import Account
except ImportError:
print("ERROR: eth_account not installed.")
print("Install with: pip install eth-account")
print("")
print("Alternative: use 'cast wallet new' (Foundry) or MetaMask.")
sys.exit(1)
acct = Account.create()
address = acct.address
private_key = acct.key.hex()
print("=" * 60)
print(" NEW BURNER WALLET CREATED")
print("=" * 60)
print(f" Address: {address}")
print(f" Private Key: {private_key}")
print("=" * 60)
print("")
print("SAVE THE PRIVATE KEY NOW — it cannot be recovered.")
print("")
print("Next steps:")
print(f" 1. Fund {address} with USDC on Polygon")
print(" - Start with $25 or less (First Time tier)")
print(" - Send ~0.1 MATIC for gas fees")
print(" 2. Set up your .env file:")
print("")
env_path = Path(__file__).parent.parent / ".env"
example_path = Path(__file__).parent.parent / ".env.example"
if not env_path.exists() and example_path.exists():
print(f" cp {example_path} {env_path}")
print(f" chmod 600 {env_path}")
print(f" # Edit {env_path} and paste your private key")
else:
print(f" export POLYMARKET_PRIVATE_KEY={private_key}")
print(" export POLYMARKET_CONFIRM=true")
print(" export POLYMARKET_MAX_SIZE=5")
print(" export POLYMARKET_DAILY_LOSS_LIMIT=10")
print("")
print(" 3. Verify setup:")
print(" python setup_wallet.py --verify")
print("")
print("SECURITY REMINDERS:")
print(" - This is a BURNER wallet — only fund what you can lose")
print(" - NEVER use your main wallet's private key")
print(" - NEVER commit .env to git")
print(" - NEVER paste the private key into chat")
def verify_config():
"""Check that all required env vars are set and valid."""
print("=" * 60)
print(" LIVE TRADING CONFIGURATION CHECK")
print("=" * 60)
print("")
checks = []
# Check POLYMARKET_PRIVATE_KEY
pk = os.environ.get("POLYMARKET_PRIVATE_KEY", "")
if not pk:
checks.append(("POLYMARKET_PRIVATE_KEY", "MISSING", "Not set"))
elif not pk.startswith("0x") or len(pk) != 66:
checks.append(("POLYMARKET_PRIVATE_KEY", "INVALID", "Must be 0x + 64 hex chars"))
else:
masked = pk[:6] + "..." + pk[-4:]
checks.append(("POLYMARKET_PRIVATE_KEY", "OK", f"Set ({masked})"))
# Check POLYMARKET_CONFIRM
confirm = os.environ.get("POLYMARKET_CONFIRM", "")
if confirm != "true":
checks.append(("POLYMARKET_CONFIRM", "MISSING", f"Got '{confirm}', need 'true'"))
else:
checks.append(("POLYMARKET_CONFIRM", "OK", "Safety gate enabled"))
# Check POLYMARKET_MAX_SIZE
max_size = os.environ.get("POLYMARKET_MAX_SIZE", "")
if not max_size:
checks.append(("POLYMARKET_MAX_SIZE", "DEFAULT", "Not set, will use $10 default"))
else:
try:
val = float(max_size)
tier = "First time" if val <= 5 else "Learning" if val <= 10 else "Experienced" if val <= 50 else "Advanced"
checks.append(("POLYMARKET_MAX_SIZE", "OK", f"${val:.0f} per trade ({tier} tier)"))
except ValueError:
checks.append(("POLYMARKET_MAX_SIZE", "INVALID", f"Got '{max_size}', need a number"))
# Check POLYMARKET_DAILY_LOSS_LIMIT
dll = os.environ.get("POLYMARKET_DAILY_LOSS_LIMIT", "")
if not dll:
checks.append(("POLYMARKET_DAILY_LOSS_LIMIT", "DEFAULT", "Not set, will use $50 default"))
else:
try:
val = float(dll)
checks.append(("POLYMARKET_DAILY_LOSS_LIMIT", "OK", f"${val:.0f} daily limit"))
except ValueError:
checks.append(("POLYMARKET_DAILY_LOSS_LIMIT", "INVALID", f"Got '{dll}', need a number"))
# Check .env file permissions
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
mode = oct(stat.S_IMODE(env_path.stat().st_mode))
if mode == "0o600":
checks.append((".env permissions", "OK", f"{mode} (owner read/write only)"))
else:
checks.append((".env permissions", "WARN", f"{mode} — should be 0o600. Run: chmod 600 {env_path}"))
else:
checks.append((".env file", "INFO", "No .env file found (using shell env vars)"))
# Check .gitignore
gitignore_path = Path(__file__).parent.parent.parent / ".gitignore"
if gitignore_path.exists():
content = gitignore_path.read_text()
if ".env" in content:
checks.append((".gitignore", "OK", ".env is gitignored"))
else:
checks.append((".gitignore", "WARN", ".env NOT in .gitignore — add it!"))
# Print results
all_ok = True
for name, status, detail in checks:
icon = {"OK": "+", "MISSING": "X", "INVALID": "X", "WARN": "!", "DEFAULT": "~", "INFO": "-"}
print(f" [{icon.get(status, '?')}] {name}: {detail}")
if status in ("MISSING", "INVALID"):
all_ok = False
print("")
if all_ok:
print(" STATUS: READY for live trading")
print("")
print(" Before your first trade, also run:")
print(" python setup_wallet.py --check-balance")
print(" python check_positions.py --balance")
else:
print(" STATUS: NOT READY — fix the issues above")
return all_ok
def check_balance():
"""Query on-chain USDC balance for the configured wallet."""
pk = os.environ.get("POLYMARKET_PRIVATE_KEY", "")
if not pk or not pk.startswith("0x") or len(pk) != 66:
print("ERROR: POLYMARKET_PRIVATE_KEY not set or invalid.")
print("Run: python setup_wallet.py --verify")
sys.exit(1)
try:
from eth_account import Account
acct = Account.from_key(pk)
address = acct.address
except ImportError:
print("ERROR: eth_account not installed. pip install eth-account")
sys.exit(1)
try:
import requests
except ImportError:
print("ERROR: requests not installed. pip install requests")
sys.exit(1)
print(f"Checking balance for: {address}")
print("")
# Check MATIC balance
payload = {
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": [address, "latest"],
"id": 1,
}
try:
resp = requests.post(POLYGON_RPC, json=payload, timeout=10)
result = resp.json().get("result", "0x0")
matic_wei = int(result, 16)
matic = matic_wei / 1e18
matic_status = "OK" if matic >= 0.01 else "LOW — need ~0.1 MATIC for gas"
print(f" MATIC: {matic:.4f} ({matic_status})")
except Exception as e:
print(f" MATIC: ERROR fetching — {e}")
# Check USDC balance (ERC20 balanceOf)
# balanceOf(address) selector = 0x70a08231
padded_addr = address[2:].lower().zfill(64)
payload = {
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{"to": USDC_CONTRACT, "data": f"0x70a08231{padded_addr}"},
"latest",
],
"id": 2,
}
try:
resp = requests.post(POLYGON_RPC, json=payload, timeout=10)
result = resp.json().get("result", "0x0")
usdc_raw = int(result, 16)
usdc = usdc_raw / 1e6 # USDC has 6 decimals
tier = "First time" if usdc <= 25 else "Learning" if usdc <= 100 else "Experienced" if usdc <= 500 else "Advanced"
print(f" USDC: ${usdc:.2f} ({tier} tier)")
except Exception as e:
print(f" USDC: ERROR fetching — {e}")
print("")
print("If balance is zero, fund the wallet:")
print(f" Send USDC (Polygon) to: {address}")
print(" Send ~0.1 MATIC for gas fees")
def main():
parser = argparse.ArgumentParser(
description="Polymarket live trading wallet setup and verification"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--create", action="store_true", help="Generate a new burner wallet"
)
group.add_argument(
"--verify", action="store_true", help="Verify existing env var configuration"
)
group.add_argument(
"--check-balance",
action="store_true",
help="Check on-chain USDC and MATIC balance",
)
args = parser.parse_args()
if args.create:
create_wallet()
elif args.verify:
verify_config()
elif args.check_balance:
check_balance()
if __name__ == "__main__":
main()
+7
View File
@@ -52,6 +52,13 @@ python ~/.agents/skills/polymarket-paper-trader/scripts/portfolio_report.py
python ~/.agents/skills/polymarket-paper-trader/scripts/portfolio_report.py --json
```
### Portfolio Health Check (Session Start)
```bash
python ~/.agents/skills/polymarket-paper-trader/scripts/health_check.py
python ~/.agents/skills/polymarket-paper-trader/scripts/health_check.py --json
```
Runs the full session-start workflow in one command: loads portfolio, fetches live prices, updates DB, calculates drawdown, checks stop losses, evaluates all risk limits. Returns GREEN/YELLOW/RED status.
## Finding Token IDs
Token IDs come from the Polymarket Gamma API. To find them for a market:
+664
View File
@@ -0,0 +1,664 @@
#!/usr/bin/env python3
"""
Portfolio Health Check — Automated Session Start Workflow
Performs the complete CLAUDE.md Session Start sequence in one command:
1. Load portfolio from SQLite
2. Fetch LIVE prices from CLOB API for every open position
3. Update current_price in DB
4. Calculate per-position P&L and stop-loss status
5. Calculate portfolio-level metrics (value, drawdown, daily P&L, concentration)
6. Check graduated drawdown thresholds (10% / 15% / 20%)
7. Check daily loss limit (5%) and weekly loss limit (10%)
8. Output a clean summary with overall status: GREEN / YELLOW / RED
"""
import argparse
import json
import os
import sqlite3
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# ---------------------------------------------------------------------------
# Import paper_engine for DB access and API helpers
# ---------------------------------------------------------------------------
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
if _THIS_DIR not in sys.path:
sys.path.append(_THIS_DIR)
from paper_engine import (
DB_PATH,
CLOB_API,
_get_db,
_active_portfolio,
_validate_token_id,
_api_get,
fetch_midpoint,
)
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DEFAULT_PORTFOLIO_NAME = "default"
MAX_CONCURRENT_POSITIONS = 5
MAX_SINGLE_MARKET_PCT = 0.20 # 20%
DAILY_LOSS_LIMIT_PCT = 0.05 # 5%
WEEKLY_LOSS_LIMIT_PCT = 0.10 # 10%
DEFAULT_TRAILING_STOP_PCT = 0.15 # 15% trailing stop from entry
# Graduated drawdown thresholds from CLAUDE.md Section 2
DRAWDOWN_THRESHOLDS = [
{
"level": 0.10,
"tier": "WARN",
"label": "10% drawdown",
"action": "Reduce ALL position sizes by 50%",
"restrictions": None,
},
{
"level": 0.15,
"tier": "ALERT",
"label": "15% drawdown",
"action": "Reduce ALL position sizes by 75%; no new momentum or news trades",
"restrictions": ["no_momentum", "no_news"],
},
{
"level": 0.20,
"tier": "CRITICAL",
"label": "20% drawdown",
"action": "Close ALL positions; halt all trading; full strategy review required",
"restrictions": ["halt_all"],
},
]
# ---------------------------------------------------------------------------
# Live price fetching
# ---------------------------------------------------------------------------
def fetch_live_price(token_id: str) -> float | None:
"""
Fetch the midpoint price for a token from the CLOB API.
Returns None on failure instead of raising.
"""
try:
return fetch_midpoint(token_id)
except Exception:
return None
# ---------------------------------------------------------------------------
# Core health check logic
# ---------------------------------------------------------------------------
def run_health_check(
db_path: str | Path = DB_PATH,
portfolio_name: str = DEFAULT_PORTFOLIO_NAME,
) -> dict:
"""
Execute the full Session Start health check workflow.
Returns a structured dict containing:
- portfolio overview
- per-position details with live prices and stop-loss status
- risk utilization metrics
- alerts/warnings
- overall status (GREEN / YELLOW / RED)
"""
db_path = Path(db_path).expanduser()
if not db_path.exists():
raise RuntimeError(
f"Portfolio database not found: {db_path}\n"
f"Run: python paper_engine.py --action init"
)
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
try:
# ---------------------------------------------------------------
# 1. Load portfolio
# ---------------------------------------------------------------
pf = _active_portfolio(conn, portfolio_name)
pid = pf["id"]
starting_balance = pf["starting_balance"]
cash_balance = pf["cash_balance"]
peak_value = pf["peak_value"]
# ---------------------------------------------------------------
# 2-3. Fetch live prices and update DB for each open position
# ---------------------------------------------------------------
positions_rows = conn.execute(
"SELECT * FROM positions WHERE portfolio_id = ? AND closed = 0",
(pid,),
).fetchall()
now_iso = datetime.now(timezone.utc).isoformat()
positions = []
price_errors = []
for row in positions_rows:
p = dict(row)
token_id = p["token_id"]
live_price = fetch_live_price(token_id)
if live_price is not None:
p["current_price"] = live_price
conn.execute(
"UPDATE positions SET current_price = ?, updated_at = ? WHERE id = ?",
(live_price, now_iso, p["id"]),
)
else:
price_errors.append(token_id)
# Keep stale price from DB
positions.append(p)
conn.commit()
# ---------------------------------------------------------------
# 4. Per-position P&L and stop-loss status
# ---------------------------------------------------------------
position_details = []
positions_value = 0.0
for p in positions:
shares = p["shares"]
entry = p["avg_entry"]
current = p["current_price"]
value = shares * current
unrealized_pnl = (current - entry) * shares
pnl_pct = ((current - entry) / entry * 100) if entry > 0 else 0.0
# Stop-loss: default 15% trailing from entry
# (CLAUDE.md says stop_loss = entry_price - edge/2, but we
# don't store edge, so use 15% trailing stop from entry)
stop_price = entry * (1 - DEFAULT_TRAILING_STOP_PCT)
stop_triggered = current <= stop_price
position_details.append({
"id": p["id"],
"token_id": p["token_id"],
"market_question": p["market_question"] or "Unknown",
"side": p["side"],
"shares": round(shares, 4),
"avg_entry": round(entry, 6),
"current_price": round(current, 6),
"value": round(value, 4),
"unrealized_pnl": round(unrealized_pnl, 4),
"pnl_pct": round(pnl_pct, 2),
"stop_price": round(stop_price, 6),
"stop_triggered": stop_triggered,
"price_stale": p["token_id"] in price_errors,
"opened_at": p["opened_at"],
})
positions_value += value
# ---------------------------------------------------------------
# 5. Portfolio-level metrics
# ---------------------------------------------------------------
total_value = cash_balance + positions_value
overall_pnl = total_value - starting_balance
overall_pnl_pct = (overall_pnl / starting_balance * 100) if starting_balance > 0 else 0.0
# Update peak if we have a new high
if total_value > peak_value:
peak_value = total_value
conn.execute(
"UPDATE portfolios SET peak_value = ?, updated_at = ? WHERE id = ?",
(peak_value, now_iso, pid),
)
conn.commit()
# Drawdown from peak
drawdown_usd = peak_value - total_value
drawdown_pct = (drawdown_usd / peak_value * 100) if peak_value > 0 else 0.0
# Daily P&L: compare to yesterday's snapshot or starting balance
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
prev_snapshot = conn.execute(
"""SELECT total_value FROM daily_snapshots
WHERE portfolio_id = ? AND date < ?
ORDER BY date DESC LIMIT 1""",
(pid, today),
).fetchone()
prev_value = prev_snapshot["total_value"] if prev_snapshot else starting_balance
daily_pnl = total_value - prev_value
daily_pnl_pct = (daily_pnl / prev_value * 100) if prev_value > 0 else 0.0
# Position count
num_positions = len(position_details)
# Single market concentration: max exposure to any one token_id
exposure_by_token = {}
for p in position_details:
tid = p["token_id"]
exposure_by_token[tid] = exposure_by_token.get(tid, 0.0) + p["value"]
max_concentration = 0.0
max_concentration_market = ""
for tid, exp in exposure_by_token.items():
pct = (exp / total_value) if total_value > 0 else 0.0
if pct > max_concentration:
max_concentration = pct
# Look up market name
for p in position_details:
if p["token_id"] == tid:
max_concentration_market = p["market_question"]
break
# ---------------------------------------------------------------
# 6. Graduated drawdown thresholds
# ---------------------------------------------------------------
drawdown_fraction = drawdown_pct / 100.0
drawdown_tier = "NONE"
drawdown_action = None
for threshold in DRAWDOWN_THRESHOLDS:
if drawdown_fraction >= threshold["level"]:
drawdown_tier = threshold["tier"]
drawdown_action = threshold["action"]
# ---------------------------------------------------------------
# 7. Daily and weekly loss limits
# ---------------------------------------------------------------
# Daily realized losses from SELL/CLOSE trades today
daily_realized_row = conn.execute(
"""SELECT COALESCE(SUM(
CASE WHEN action IN ('SELL','CLOSE') AND entry_avg IS NOT NULL
THEN (price - entry_avg) * shares
ELSE 0 END
), 0) as realized
FROM trades
WHERE portfolio_id = ? AND date(executed_at) = ?""",
(pid, today),
).fetchone()
daily_realized = daily_realized_row["realized"] if daily_realized_row else 0.0
daily_loss = abs(min(0, daily_realized))
daily_loss_limit = starting_balance * DAILY_LOSS_LIMIT_PCT
daily_loss_breached = daily_loss >= daily_loss_limit
# Weekly realized losses (since last Monday)
now_dt = datetime.now(timezone.utc)
days_since_monday = now_dt.weekday() # Monday=0
last_monday = (now_dt - timedelta(days=days_since_monday)).strftime("%Y-%m-%d")
weekly_realized_row = conn.execute(
"""SELECT COALESCE(SUM(
CASE WHEN action IN ('SELL','CLOSE') AND entry_avg IS NOT NULL
THEN (price - entry_avg) * shares
ELSE 0 END
), 0) as realized
FROM trades
WHERE portfolio_id = ? AND date(executed_at) >= ?""",
(pid, last_monday),
).fetchone()
weekly_realized = weekly_realized_row["realized"] if weekly_realized_row else 0.0
weekly_loss = abs(min(0, weekly_realized))
weekly_loss_limit = starting_balance * WEEKLY_LOSS_LIMIT_PCT
weekly_loss_breached = weekly_loss >= weekly_loss_limit
# ---------------------------------------------------------------
# 8. Build alerts and determine overall status
# ---------------------------------------------------------------
alerts = []
overall_status = "GREEN"
# Stop-loss alerts
stops_triggered = [p for p in position_details if p["stop_triggered"]]
for p in stops_triggered:
alerts.append({
"severity": "HIGH",
"type": "STOP_LOSS",
"message": (
f"Stop-loss triggered for {p['side']} position in "
f"'{p['market_question'][:60]}': "
f"current ${p['current_price']:.4f} <= stop ${p['stop_price']:.4f}"
),
})
overall_status = "RED"
# Stale price warnings
if price_errors:
alerts.append({
"severity": "MEDIUM",
"type": "STALE_PRICE",
"message": (
f"Failed to fetch live prices for {len(price_errors)} position(s). "
f"Using stale cached prices."
),
})
if overall_status == "GREEN":
overall_status = "YELLOW"
# Drawdown alerts
if drawdown_tier == "WARN":
alerts.append({
"severity": "MEDIUM",
"type": "DRAWDOWN_WARN",
"message": (
f"Drawdown at {drawdown_pct:.1f}% from peak. "
f"Recommendation: {drawdown_action}"
),
})
if overall_status == "GREEN":
overall_status = "YELLOW"
elif drawdown_tier == "ALERT":
alerts.append({
"severity": "HIGH",
"type": "DRAWDOWN_ALERT",
"message": (
f"Drawdown at {drawdown_pct:.1f}% from peak. "
f"Recommendation: {drawdown_action}"
),
})
overall_status = "RED"
elif drawdown_tier == "CRITICAL":
alerts.append({
"severity": "CRITICAL",
"type": "DRAWDOWN_CRITICAL",
"message": (
f"Drawdown at {drawdown_pct:.1f}% from peak. "
f"REQUIRED ACTION: {drawdown_action}"
),
})
overall_status = "RED"
# Daily loss limit
if daily_loss_breached:
alerts.append({
"severity": "HIGH",
"type": "DAILY_LOSS_LIMIT",
"message": (
f"Daily loss limit breached: ${daily_loss:.2f} realized losses "
f"(limit: ${daily_loss_limit:.2f} = {DAILY_LOSS_LIMIT_PCT*100:.0f}% "
f"of starting balance). All new entries blocked until next UTC day."
),
})
overall_status = "RED"
# Weekly loss limit
if weekly_loss_breached:
alerts.append({
"severity": "HIGH",
"type": "WEEKLY_LOSS_LIMIT",
"message": (
f"Weekly loss limit breached: ${weekly_loss:.2f} realized losses "
f"(limit: ${weekly_loss_limit:.2f} = {WEEKLY_LOSS_LIMIT_PCT*100:.0f}% "
f"of starting balance). All new entries blocked until next Monday."
),
})
overall_status = "RED"
# Concentration warning
if max_concentration > MAX_SINGLE_MARKET_PCT:
alerts.append({
"severity": "MEDIUM",
"type": "CONCENTRATION",
"message": (
f"Single market concentration at {max_concentration*100:.1f}% "
f"(limit: {MAX_SINGLE_MARKET_PCT*100:.0f}%) in "
f"'{max_concentration_market[:60]}'"
),
})
if overall_status == "GREEN":
overall_status = "YELLOW"
# Position count warning
if num_positions >= MAX_CONCURRENT_POSITIONS:
alerts.append({
"severity": "MEDIUM",
"type": "MAX_POSITIONS",
"message": (
f"At maximum concurrent positions: "
f"{num_positions}/{MAX_CONCURRENT_POSITIONS}. "
f"No new positions allowed."
),
})
if overall_status == "GREEN":
overall_status = "YELLOW"
# ---------------------------------------------------------------
# Assemble result
# ---------------------------------------------------------------
result = {
"status": overall_status,
"checked_at": now_iso,
"portfolio": {
"name": portfolio_name,
"starting_balance": round(starting_balance, 2),
"cash_balance": round(cash_balance, 2),
"positions_value": round(positions_value, 2),
"total_value": round(total_value, 2),
"pnl": round(overall_pnl, 2),
"pnl_pct": round(overall_pnl_pct, 2),
"peak_value": round(peak_value, 2),
"drawdown_usd": round(drawdown_usd, 2),
"drawdown_pct": round(drawdown_pct, 2),
"daily_pnl": round(daily_pnl, 2),
"daily_pnl_pct": round(daily_pnl_pct, 2),
},
"positions": position_details,
"risk": {
"position_count": num_positions,
"position_limit": MAX_CONCURRENT_POSITIONS,
"position_utilization": f"{num_positions}/{MAX_CONCURRENT_POSITIONS}",
"max_concentration_pct": round(max_concentration * 100, 1),
"max_concentration_market": max_concentration_market[:70] if max_concentration_market else "N/A",
"concentration_limit_pct": MAX_SINGLE_MARKET_PCT * 100,
"drawdown_tier": drawdown_tier,
"drawdown_action": drawdown_action,
"daily_loss": round(daily_loss, 2),
"daily_loss_limit": round(daily_loss_limit, 2),
"daily_loss_pct": round(daily_loss / starting_balance * 100, 2) if starting_balance > 0 else 0.0,
"daily_loss_breached": daily_loss_breached,
"weekly_loss": round(weekly_loss, 2),
"weekly_loss_limit": round(weekly_loss_limit, 2),
"weekly_loss_pct": round(weekly_loss / starting_balance * 100, 2) if starting_balance > 0 else 0.0,
"weekly_loss_breached": weekly_loss_breached,
"stops_triggered": len(stops_triggered),
},
"alerts": alerts,
}
return result
finally:
conn.close()
# ---------------------------------------------------------------------------
# Text formatting
# ---------------------------------------------------------------------------
def format_human_readable(result: dict) -> str:
"""Format the health check result as a human-readable report."""
pf = result["portfolio"]
risk = result["risk"]
status = result["status"]
# Status indicator
status_bar = {
"GREEN": "[GREEN] All systems nominal",
"YELLOW": "[YELLOW] Caution -- review warnings below",
"RED": "[RED] Action required -- review alerts below",
}
lines = [
"=" * 64,
f" PORTFOLIO HEALTH CHECK",
f" {result['checked_at'][:19]} UTC",
f" Status: {status_bar.get(status, status)}",
"=" * 64,
"",
"--- Portfolio Overview ---",
f" Starting Balance: ${pf['starting_balance']:>12,.2f}",
f" Cash: ${pf['cash_balance']:>12,.2f}",
f" Positions Value: ${pf['positions_value']:>12,.2f}",
f" Total Value: ${pf['total_value']:>12,.2f}",
f" P&L: ${pf['pnl']:>12,.2f} ({pf['pnl_pct']:+.2f}%)",
f" Peak Value: ${pf['peak_value']:>12,.2f}",
f" Drawdown: ${pf['drawdown_usd']:>12,.2f} ({pf['drawdown_pct']:.2f}%)",
f" Daily P&L: ${pf['daily_pnl']:>12,.2f} ({pf['daily_pnl_pct']:+.2f}%)",
]
# --- Positions ---
if result["positions"]:
lines.append("")
lines.append("--- Open Positions ---")
lines.append(
f" {'Side':>4} {'Shares':>8} {'Entry':>8} {'Current':>8} "
f"{'P&L':>10} {'P&L%':>7} {'Stop':>8} {'Status':>8}"
)
lines.append(" " + "-" * 74)
for p in result["positions"]:
stop_status = "STOP!" if p["stop_triggered"] else "OK"
stale = " [stale]" if p["price_stale"] else ""
lines.append(
f" {p['side']:>4} {p['shares']:>8.2f} "
f"${p['avg_entry']:>.4f} ${p['current_price']:>.4f} "
f"${p['unrealized_pnl']:>+9,.2f} {p['pnl_pct']:>+6.1f}% "
f"${p['stop_price']:>.4f} {stop_status:>8}{stale}"
)
# Market question on its own line
lines.append(f" {p['market_question'][:60]}")
else:
lines.append("")
lines.append("--- Open Positions ---")
lines.append(" No open positions.")
# --- Risk Utilization ---
lines.append("")
lines.append("--- Risk Utilization ---")
lines.append(
f" Positions: {risk['position_utilization']:>12}"
)
lines.append(
f" Max Concentration: {risk['max_concentration_pct']:>11.1f}% "
f"(limit: {risk['concentration_limit_pct']:.0f}%)"
)
if risk["max_concentration_market"] != "N/A":
lines.append(
f" in: {risk['max_concentration_market']}"
)
lines.append(
f" Drawdown Tier: {risk['drawdown_tier']:>12}"
)
if risk["drawdown_action"]:
lines.append(f" Action: {risk['drawdown_action']}")
lines.append(
f" Daily Loss: ${risk['daily_loss']:>11,.2f} / "
f"${risk['daily_loss_limit']:,.2f} "
f"({risk['daily_loss_pct']:.1f}% / {DAILY_LOSS_LIMIT_PCT*100:.0f}%)"
)
lines.append(
f" Weekly Loss: ${risk['weekly_loss']:>11,.2f} / "
f"${risk['weekly_loss_limit']:,.2f} "
f"({risk['weekly_loss_pct']:.1f}% / {WEEKLY_LOSS_LIMIT_PCT*100:.0f}%)"
)
lines.append(
f" Stops Triggered: {risk['stops_triggered']:>12}"
)
# --- Alerts ---
if result["alerts"]:
lines.append("")
lines.append("--- Alerts ---")
for alert in result["alerts"]:
severity = alert["severity"]
lines.append(f" [{severity}] {alert['message']}")
else:
lines.append("")
lines.append("--- Alerts ---")
lines.append(" None. All risk checks passed.")
lines.append("")
lines.append("=" * 64)
lines.append(
f" OVERALL STATUS: {status}"
)
lines.append("=" * 64)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Portfolio health check — automated Session Start workflow",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Performs the complete CLAUDE.md Session Start sequence:
1. Load portfolio from SQLite
2. Fetch live prices from CLOB API for every open position
3. Update current_price in DB
4. Calculate per-position P&L and stop-loss status
5. Check graduated drawdown thresholds (10/15/20%%)
6. Check daily loss (5%%) and weekly loss (10%%) limits
7. Output status: GREEN / YELLOW / RED
Examples:
%(prog)s
%(prog)s --portfolio my_portfolio
%(prog)s --json
%(prog)s --portfolio-db /path/to/portfolio.db --json
""",
)
parser.add_argument(
"--portfolio-db",
type=str,
default=str(DB_PATH),
help=f"Path to portfolio SQLite database (default: {DB_PATH})",
)
parser.add_argument(
"--portfolio",
type=str,
default=DEFAULT_PORTFOLIO_NAME,
help="Portfolio name (default: 'default')",
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON instead of human-readable format",
)
args = parser.parse_args()
try:
result = run_health_check(
db_path=args.portfolio_db,
portfolio_name=args.portfolio,
)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_human_readable(result))
# Exit code reflects status: 0=GREEN, 1=YELLOW, 2=RED
exit_codes = {"GREEN": 0, "YELLOW": 1, "RED": 2}
sys.exit(exit_codes.get(result["status"], 1))
except RuntimeError as exc:
if args.json:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
else:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(3)
if __name__ == "__main__":
main()
+17
View File
@@ -179,6 +179,23 @@ source /home/verticalclaw/.venv/bin/activate && python polymarket-strategy-advis
Output: JSON array of trade recommendations sorted by expected value.
### Backtest Engine (`scripts/backtest.py`)
Comprehensive performance analysis and live-readiness assessment:
```bash
source /home/verticalclaw/.venv/bin/activate && python polymarket-strategy-advisor/scripts/backtest.py
```
Live-readiness check only:
```bash
source /home/verticalclaw/.venv/bin/activate && python polymarket-strategy-advisor/scripts/backtest.py --live-check
```
Output: total return, win rate, Sharpe ratio, max drawdown, profit factor,
per-strategy breakdown, and READY/NOT READY assessment against CLAUDE.md
prerequisites (20+ trades, >55% win rate, Sharpe >0.5, drawdown <15%).
### Daily Performance Review (`scripts/daily_review.py`)
Analyzes paper trading history and suggests improvements:
File diff suppressed because it is too large Load Diff