Files
polymarket-skills/polymarket-strategy-advisor/scripts/daily_review.py
T
Polymarket Skills Builder 068b2adc75 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>
2026-02-26 07:25:07 +00:00

409 lines
14 KiB
Python

#!/usr/bin/env python3
"""Analyze paper trading performance and suggest parameter adjustments.
Reads the paper trader's SQLite database, computes performance metrics,
breaks down results by strategy type, and outputs actionable suggestions.
Usage:
python daily_review.py --portfolio-db ~/.polymarket-paper/portfolio.db
python daily_review.py --portfolio-db ~/.polymarket-paper/portfolio.db --days 7
"""
import argparse
import json
import math
import os
import sqlite3
import sys
from datetime import datetime, timedelta, timezone
DEFAULT_DB_PATH = os.path.expanduser("~/.polymarket-paper/portfolio.db")
def connect_db(db_path):
"""Connect to the paper trader database. Returns None if not found."""
if not os.path.exists(db_path):
return None
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
def get_closed_trades(conn, since_date):
"""Fetch all closed (SELL) trades since a given date.
The paper_engine schema stores BUY and SELL as separate trade rows.
A 'closed' trade is a SELL action. We join with the position to get
entry price for P&L calculation.
"""
try:
cur = conn.cursor()
cur.execute(
"""
SELECT
t.market_question,
t.side,
t.action,
t.price as exit_price,
t.shares,
t.total_cost,
t.fee,
t.reasoning,
t.executed_at as closed_at,
-- Calculate realized P&L: for SELL trades, profit = (sell_price - avg_entry) * shares
COALESCE(
(SELECT b.price FROM trades b
WHERE b.token_id = t.token_id AND b.action = 'BUY'
AND b.portfolio_id = t.portfolio_id
ORDER BY b.executed_at DESC LIMIT 1),
t.price
) as entry_price
FROM trades t
WHERE t.action = 'SELL'
AND t.executed_at >= ?
ORDER BY t.executed_at DESC
""",
(since_date.isoformat(),),
)
rows = [dict(row) for row in cur.fetchall()]
# Calculate realized P&L for each trade
for r in rows:
entry = float(r.get("entry_price", 0))
exit_p = float(r.get("exit_price", 0))
shares = float(r.get("shares", 0))
fee = float(r.get("fee", 0))
r["realized_pnl"] = round((exit_p - entry) * shares - fee, 4)
return rows
except sqlite3.OperationalError:
return []
def get_open_positions(conn):
"""Fetch currently open positions."""
try:
cur = conn.cursor()
cur.execute(
"""
SELECT
market_question,
token_id,
side,
avg_entry as entry_price,
shares,
current_price,
opened_at
FROM positions
WHERE closed = 0
ORDER BY opened_at DESC
"""
)
return [dict(row) for row in cur.fetchall()]
except sqlite3.OperationalError:
return []
def get_account_history(conn, since_date):
"""Fetch portfolio value history from daily_snapshots."""
try:
cur = conn.cursor()
cur.execute(
"""
SELECT total_value as portfolio_value, cash_balance as cash, date as updated_at
FROM daily_snapshots
WHERE date >= ?
ORDER BY date ASC
""",
(since_date.strftime("%Y-%m-%d"),),
)
return [dict(row) for row in cur.fetchall()]
except sqlite3.OperationalError:
return []
def compute_metrics(trades):
"""Compute performance metrics from a list of closed trades."""
if not trades:
return {
"total_trades": 0,
"winners": 0,
"losers": 0,
"win_rate": 0.0,
"total_pnl": 0.0,
"avg_pnl": 0.0,
"avg_winner": 0.0,
"avg_loser": 0.0,
"largest_winner": 0.0,
"largest_loser": 0.0,
"profit_factor": 0.0,
"avg_hold_time_hours": 0.0,
}
pnls = [float(t.get("realized_pnl", 0)) for t in trades]
winners = [p for p in pnls if p > 0]
losers = [p for p in pnls if p < 0]
total_pnl = sum(pnls)
gross_profit = sum(winners) if winners else 0
gross_loss = abs(sum(losers)) if losers else 0
# Hold time calculation
hold_times = []
for t in trades:
opened = t.get("opened_at", "")
closed = t.get("closed_at", "")
if opened and closed:
try:
o = datetime.fromisoformat(opened)
c = datetime.fromisoformat(closed)
hold_times.append((c - o).total_seconds() / 3600)
except (ValueError, TypeError):
pass
return {
"total_trades": len(trades),
"winners": len(winners),
"losers": len(losers),
"breakeven": len(trades) - len(winners) - len(losers),
"win_rate": len(winners) / len(trades) if trades else 0,
"total_pnl": round(total_pnl, 2),
"avg_pnl": round(total_pnl / len(trades), 2) if trades else 0,
"avg_winner": round(gross_profit / len(winners), 2) if winners else 0,
"avg_loser": round(sum(losers) / len(losers), 2) if losers else 0,
"largest_winner": round(max(winners), 2) if winners else 0,
"largest_loser": round(min(losers), 2) if losers else 0,
"profit_factor": round(gross_profit / gross_loss, 2) if gross_loss > 0 else float("inf"),
"avg_hold_time_hours": round(sum(hold_times) / len(hold_times), 1) if hold_times else 0,
}
def compute_drawdown(account_history):
"""Compute max drawdown from account history."""
if not account_history:
return {"max_drawdown_pct": 0, "current_drawdown_pct": 0}
values = [float(h["portfolio_value"]) for h in account_history]
peak = values[0]
max_dd = 0
for v in values:
peak = max(peak, v)
dd = (peak - v) / peak if peak > 0 else 0
max_dd = max(max_dd, dd)
current_peak = max(values)
current_dd = (current_peak - values[-1]) / current_peak if current_peak > 0 else 0
return {
"max_drawdown_pct": round(max_dd * 100, 2),
"current_drawdown_pct": round(current_dd * 100, 2),
}
def breakdown_by_strategy(trades):
"""Break down metrics by edge_type."""
strategies = {}
for t in trades:
edge_type = t.get("edge_type", "unknown") or "unknown"
if edge_type not in strategies:
strategies[edge_type] = []
strategies[edge_type].append(t)
result = {}
for strategy, strades in strategies.items():
result[strategy] = compute_metrics(strades)
return result
def generate_suggestions(metrics, strategy_breakdown, drawdown, open_positions):
"""Generate actionable parameter adjustment suggestions."""
suggestions = []
# Overall performance
if metrics["total_trades"] == 0:
suggestions.append(
"No closed trades in this period. Start by running the advisor "
"to find opportunities and executing paper trades."
)
return suggestions
# Win rate analysis
if metrics["win_rate"] < 0.40 and metrics["total_trades"] >= 10:
suggestions.append(
f"Win rate is {metrics['win_rate']:.0%} (below 40% threshold). "
f"Consider tightening entry criteria: increase --min-edge to 0.05 "
f"or raise minimum confidence to 0.7."
)
elif metrics["win_rate"] > 0.70 and metrics["total_trades"] >= 10:
suggestions.append(
f"Win rate is {metrics['win_rate']:.0%} (strong). Consider "
f"slightly increasing position sizes if risk limits allow."
)
# Profit factor
if metrics["profit_factor"] < 1.0 and metrics["total_trades"] >= 5:
suggestions.append(
f"Profit factor is {metrics['profit_factor']:.2f} (below 1.0 = "
f"losing money). Review: are stop losses being honored? Are "
f"winners being closed too early?"
)
# Winner/loser ratio
if metrics["avg_winner"] != 0 and metrics["avg_loser"] != 0:
wl_ratio = abs(metrics["avg_winner"] / metrics["avg_loser"])
if wl_ratio < 1.0:
suggestions.append(
f"Average winner (${metrics['avg_winner']:.2f}) is smaller than "
f"average loser (${metrics['avg_loser']:.2f}). Widen profit "
f"targets or tighten stop losses."
)
# Strategy-specific
for strategy, sm in strategy_breakdown.items():
if sm["total_trades"] >= 5 and sm["win_rate"] < 0.35:
suggestions.append(
f"Strategy '{strategy}' has {sm['win_rate']:.0%} win rate over "
f"{sm['total_trades']} trades. Consider pausing this strategy "
f"or reviewing its entry criteria."
)
# Drawdown
if drawdown["current_drawdown_pct"] > 15:
suggestions.append(
f"Current drawdown is {drawdown['current_drawdown_pct']:.1f}%. "
f"Approaching 20% stop-trading threshold. Reduce position sizes "
f"by 50% immediately."
)
elif drawdown["max_drawdown_pct"] > 10:
suggestions.append(
f"Max drawdown reached {drawdown['max_drawdown_pct']:.1f}% this "
f"period. Review whether position sizing is appropriate."
)
# Open position count
if len(open_positions) >= 5:
suggestions.append(
f"Currently at {len(open_positions)} open positions (maximum). "
f"Close existing positions before opening new ones."
)
# Hold time
if metrics["avg_hold_time_hours"] > 48:
suggestions.append(
f"Average hold time is {metrics['avg_hold_time_hours']:.0f} hours. "
f"Momentum and news-driven trades should exit within 15 minutes "
f"to 48 hours. Review if time-based exits are being enforced."
)
if not suggestions:
suggestions.append(
"Performance looks healthy. Continue with current parameters. "
"Review again after 20+ more trades for statistical significance."
)
return suggestions
def format_review(metrics, strategy_breakdown, drawdown, open_positions,
suggestions, days, trades):
"""Format the review as structured output."""
output = {
"review_date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"period_days": days,
"overall_metrics": metrics,
"drawdown": drawdown,
"strategy_breakdown": strategy_breakdown,
"open_positions": len(open_positions),
"suggestions": suggestions,
}
# Include the 5 most recent trades for context
recent = []
for t in trades[:5]:
recent.append({
"market": t.get("market_question", ""),
"side": t.get("side", ""),
"edge_type": t.get("edge_type", ""),
"pnl": float(t.get("realized_pnl", 0)),
"entry": float(t.get("entry_price", 0)),
"exit": float(t.get("exit_price", 0)),
"closed_at": t.get("closed_at", ""),
})
if recent:
output["recent_trades"] = recent
return output
def generate_sample_review():
"""Generate a sample review when no database is available."""
return {
"review_date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"period_days": 1,
"status": "no_database",
"message": (
"No paper trading database found. To generate a real performance "
"review, first execute some paper trades using the "
"polymarket-paper-trader skill. The database will be created at "
"~/.polymarket-paper/portfolio.db."
),
"overall_metrics": {
"total_trades": 0,
"winners": 0,
"losers": 0,
"win_rate": 0.0,
"total_pnl": 0.0,
},
"suggestions": [
"Start by running: python polymarket-strategy-advisor/scripts/advisor.py --top 5",
"Use the recommendations to place paper trades via the paper-trader skill.",
"After 10+ trades, run this review again for meaningful analysis.",
],
}
def main():
parser = argparse.ArgumentParser(
description="Analyze paper trading performance and suggest improvements"
)
parser.add_argument(
"--portfolio-db", type=str, default=DEFAULT_DB_PATH,
help=f"Path to paper trader SQLite database (default: {DEFAULT_DB_PATH})"
)
parser.add_argument(
"--days", type=int, default=1,
help="Number of days to review (default: 1)"
)
args = parser.parse_args()
conn = connect_db(args.portfolio_db)
if conn is None:
print(json.dumps(generate_sample_review(), indent=2))
return
since = datetime.now(timezone.utc) - timedelta(days=args.days)
trades = get_closed_trades(conn, since)
open_positions = get_open_positions(conn)
account_history = get_account_history(conn, since)
metrics = compute_metrics(trades)
strategy_breakdown = breakdown_by_strategy(trades)
drawdown = compute_drawdown(account_history)
suggestions = generate_suggestions(
metrics, strategy_breakdown, drawdown, open_positions
)
output = format_review(
metrics, strategy_breakdown, drawdown, open_positions,
suggestions, args.days, trades,
)
conn.close()
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()