dynamic sizing: 4% of equity per bet, drawdown brake, per-event correlation cap

- engine (copytrade): stake_usd() = bankroll_pct x current equity (cash + open
  cost basis) - Kelly-style compounding both directions - halved while equity
  sits below 80% of its high-water mark; new risk.max_per_event (default 2)
  blocks stacking correlated markets on one real-world event (dated-slug prefix
  grouping; LSB1 once put 6 conviction bets on a single match).
- copybot: feed/summary report the dynamic stake, stake_pct, event_cap, hwm.
- portfolio backtest mirrors the exact same rule (PCT 4%, clamp $5-$150,
  EVENT_CAP 2, brake 80%/half), with per-bet stakes in every table row and a
  persistent CLOB slug cache for event grouping. June backfill: +426% vs +168%
  flat - compounding amplifies the in-sample month; July live is the test.
  Misses fell 62 -> 19 cash-missed (+26 deliberate event-cap skips): smaller
  early stakes capture more signals.
- configs: bankroll_pct 0.04, max_trade/max_position 150 (runaway guards),
  max_per_event 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-02 10:54:07 -04:00
parent 76c83a58da
commit 716daeccc1
6 changed files with 163 additions and 41 deletions
+52 -6
View File
@@ -30,6 +30,7 @@ Usage
import argparse
import json
import os
import re
import sys
import time
import urllib.error
@@ -49,8 +50,10 @@ DEFAULT_CONFIG = {
"poll_seconds": 12, # how often to check each wallet
"discord_webhook": "", # paste a Discord webhook URL to get pings
"watchlist": [], # ["0xwallet1", "0xwallet2", ...]
"bankroll_usd": 1000.0, # your stake pool
"bankroll_pct": 0.02, # 2% of bankroll per new entry
"bankroll_usd": 1000.0, # starting stake pool
"bankroll_pct": 0.02, # fraction of CURRENT equity per new entry
# (compounds up and down; falls back to a flat
# fraction of bankroll_usd when cash isn't tracked)
"price_guard_pct": 0.05, # skip if price moved >5% from their fill
"risk": {
"max_trade_usd": 50.0, # hard ceiling on any single copy
@@ -58,6 +61,8 @@ DEFAULT_CONFIG = {
"daily_spend_cap_usd": 250.0,
"max_total_exposure_usd": 500.0,
"max_open_positions": 20,
"max_per_event": 2, # max concurrent positions on one real-world
# event (a game's markets are one correlated bet)
"min_price": 0.05, # don't open longshots/near-certainties
"max_price": 0.95,
"min_order_usd": 5.0, # Polymarket min order size
@@ -155,6 +160,16 @@ def recent_trades(wallet, limit=100):
{"user": wallet, "type": "TRADE", "limit": limit}) or []
def event_key(t):
"""Correlation-group id for a trade: the real-world event its market belongs
to. Polymarket sub-splits one game across several eventSlugs
(`…-2026-07-01-more-markets`, `…-2026-07-01-second-half-result`), so dated
slugs collapse to their `…-YYYY-MM-DD` prefix; undated slugs stand as-is."""
ev = t.get("eventSlug") or t.get("slug") or ""
m = re.match(r"(.*?\d{4}-\d{2}-\d{2})", ev)
return m.group(1) if m else (ev or None)
# ── execution ────────────────────────────────────────────────────────────────
class PaperExecutor:
@@ -243,6 +258,26 @@ class CopyTrader:
def open_exposure(self):
return sum(p["cost"] for p in self.state["my_pos"].values())
# ---- dynamic sizing: fraction of CURRENT equity, with a drawdown brake ----
DD_THRESHOLD = 0.80 # below 80% of the high-water mark…
DD_FACTOR = 0.5 # …bet half size until equity recovers
def stake_usd(self):
"""Next bet size = bankroll_pct × current equity (cash + open cost basis),
so stakes compound with the book in both directions; halved while in a
>20% drawdown from the high-water mark. Falls back to the flat static
stake when cash isn't tracked (legacy poll CLI)."""
cash = self.state.get("cash")
if cash is None:
return self.cfg["bankroll_usd"] * self.cfg["bankroll_pct"]
eq = cash + self.open_exposure()
hwm = max(self.state.get("hwm", 0.0), eq)
self.state["hwm"] = hwm
frac = self.cfg["bankroll_pct"]
if eq < self.DD_THRESHOLD * hwm:
frac *= self.DD_FACTOR
return frac * eq
def persist(self):
self.state["seen_tx"] = list(self.seen)[-5000:]
save_json(self.state_path, self.state)
@@ -295,7 +330,7 @@ class CopyTrader:
if side == "BUY":
self._handle_their_buy(wallet, token, their_size, their_price,
label, title, outcome)
label, title, outcome, event=event_key(t))
their_book[token] = their_prev + their_size
elif side == "SELL":
self._handle_their_sell(token, their_size, their_prev, label)
@@ -317,7 +352,7 @@ class CopyTrader:
return drift <= self.cfg["price_guard_pct"]
def _handle_their_buy(self, wallet, token, their_size, their_price,
label, title, outcome):
label, title, outcome, event=None):
mine = self.state["my_pos"].get(token)
is_add = mine is not None
# don't backfill: never open a position they already held when we
@@ -326,6 +361,17 @@ class CopyTrader:
if not is_add and token in self.state["seed_tokens"].get(wallet, []):
self.log(f"BUY {label} — skip (held before we started, no backfill)")
return
# correlation cap: a game's markets settle together — N bets on one event
# are one big bet, not N diversified ones (LSB1 once stacked 6 markets on
# a single match). Cap concurrent positions per real-world event.
cap = self.risk.get("max_per_event")
if not is_add and event and cap:
held = sum(1 for p in self.state["my_pos"].values()
if p.get("event") == event)
if held >= cap:
self.log(f"BUY {label} — skip (already {held} positions on this "
f"event, cap {cap})")
return
price = self._live_price(token, "buy")
if price is None:
@@ -343,7 +389,7 @@ class CopyTrader:
want_usd = want_shares * price
kind = "ADD "
else:
want_usd = self.cfg["bankroll_usd"] * self.cfg["bankroll_pct"]
want_usd = self.stake_usd() # fraction of current equity (compounds)
kind = "OPEN"
pos_cost = mine["cost"] if is_add else 0.0
@@ -364,7 +410,7 @@ class CopyTrader:
else:
self.state["my_pos"][token] = {
"shares": res["filled_shares"], "cost": spent,
"title": title, "outcome": outcome}
"title": title, "outcome": outcome, "event": event}
tag = "[PAPER]" if not self.ex.live else "[LIVE]"
self.alert(
f"{kind} {label}{tag} buy {res['filled_shares']:.1f} "