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:
@@ -1,3 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.polymarket-paper/
|
||||
.polymarket-live/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.key
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
Reference in New Issue
Block a user