profit ratchet: stakes pin at $250, surplus swept to a banked reserve

Once working equity exceeds stake_cap/pct ($6,250 at 4%), surplus cash sweeps
to state["reserve"]/portfolio reserve - banked, never bet, immune to drawdown
- keeping stakes at the $250 level where marketable fills sit inside typical
book depth. Feed/summary gain reserve; realized = cash+exposure+reserve-bank.
June backfill: equity $15,684 (+1468%), $9,185 banked, next stake pinned $250.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-03 00:49:21 -04:00
parent beb7e9d22c
commit 7e9ccdf5c8
5 changed files with 56 additions and 21 deletions
+24 -6
View File
@@ -54,6 +54,10 @@ DEFAULT_CONFIG = {
"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)
"stake_cap_usd": 0, # >0: pin stakes at this size once the book grows
# past cap/bankroll_pct — surplus cash is SWEPT to
# state["reserve"], a banked pot that never bets
# (profit ratchet + keeps fills inside book depth)
"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
@@ -264,20 +268,34 @@ class CopyTrader:
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)."""
"""Next bet size = bankroll_pct × current WORKING 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).
stake_cap_usd (profit ratchet): once working equity exceeds
cap/bankroll_pct — the level where stakes hit the cap — the surplus CASH
is swept into state["reserve"]: banked, never bet, immune to drawdowns.
Stakes stay pinned ~at the cap, where marketable fills are still inside
typical book depth."""
cash = self.state.get("cash")
if cash is None:
return self.cfg["bankroll_usd"] * self.cfg["bankroll_pct"]
frac = self.cfg["bankroll_pct"]
cap = self.cfg.get("stake_cap_usd") or 0
eq = cash + self.open_exposure()
if cap and frac > 0 and eq > cap / frac:
sweep = min(cash, eq - cap / frac)
if sweep > 0:
self.state["cash"] = cash = cash - sweep
self.state["reserve"] = self.state.get("reserve", 0.0) + sweep
eq -= sweep
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
stake = frac * eq
return min(stake, cap) if cap else stake
def record_miss(self, wallet, token, cond, title, outcome, price, want, reason):
"""A bet the strategy WOULD have copied but the book couldn't take —
+8 -4
View File
@@ -407,7 +407,9 @@ class Copybot:
"event_cap": self.engine.risk.get("max_per_event"),
"hwm": round(st.get("hwm", 0.0), 2),
"cash": round(cash, 2), "deployed": round(exp, 2),
"realized": round(cash + exp - bank, 2), "open_count": len(mp),
"reserve": round(st.get("reserve", 0.0), 2), # banked profit, never bet
"realized": round(cash + exp + st.get("reserve", 0.0) - bank, 2),
"open_count": len(mp),
"fees_paid": round(st.get("fees_paid", 0.0), 2),
"fee_rate": self.fee_rate,
"lag": {"n": lag.get("n", 0),
@@ -504,15 +506,17 @@ class Copybot:
stake = self.engine.stake_usd() # dynamic: pct of current equity
exp = self.engine.open_exposure()
cash = self.engine.state.get("cash", bank)
realized = cash + exp - bank # see _drain_fills / settle_resolved
reserve = self.engine.state.get("reserve", 0.0)
realized = cash + exp + reserve - bank # see _drain_fills / settle_resolved
n = len(self.engine.state["my_pos"])
lag = self.engine.state.get("lag", {})
lagstr = ""
if lag.get("n"):
lagstr = (f" · {lag['n']} copies avg lag {lag['sum_s']/lag['n']:.0f}s "
f"slip {lag['sum_slip_pct']/lag['n']:+.1%}")
log(f"[{cycle}] open {n} · deployed ${exp:,.0f} · free ${cash:,.0f}/${bank:,.0f} "
f"· realized ${realized:+,.2f}{lagstr}"
bankstr = f" · banked ${reserve:,.0f}" if reserve else ""
log(f"[{cycle}] open {n} · deployed ${exp:,.0f} · free ${cash:,.0f}/${bank:,.0f}"
f"{bankstr} · realized ${realized:+,.2f}{lagstr}"
+ (f" · CAN'T OPEN (free < ${stake:,.0f} stake — bets missed)"
if cash < stake else ""))
+2 -1
View File
@@ -55,5 +55,6 @@
"max_price": 0.99,
"min_order_usd": 5.0,
"max_per_event": 0
}
},
"stake_cap_usd": 250.0
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+21 -9
View File
@@ -47,7 +47,10 @@ GAMMA = "https://gamma-api.polymarket.com"
# event (a game's markets settle together — one correlated bet, not N diversified
# ones); 0 = off, mirror every conviction trade.
PCT = 0.04
STAKE_MIN, STAKE_CAP = 5.0, float("inf") # uncapped — 4% of equity rides fully
# stakes pin at STAKE_CAP (where marketable fills still sit inside typical book
# depth); once working equity exceeds STAKE_CAP/PCT, surplus cash is SWEPT into
# a banked reserve — a profit ratchet that never bets and can't be drawn down.
STAKE_MIN, STAKE_CAP = 5.0, 250.0
EVENT_CAP = 0
DD_THRESHOLD, DD_FACTOR = 0.80, 0.5
# skip entries above this price. High-price favorites win pennies and lose
@@ -207,6 +210,7 @@ def main():
fees_paid = 0.0
hwm = BANK
capped = 0
reserve = 0.0
held = [] # (free_t, cost, payoff) cost = stake + entry fee; payoff paid at free_t
perW = {w["wallet"]: {"name": w["name"], "wallet": w["wallet"], "bets": 0,
"won": 0, "lost": 0,
@@ -214,9 +218,15 @@ def main():
resolved, current, missed = [], [], []
def cur_stake():
"""PCT of current equity, drawdown-braked, clamped — the copybot's rule."""
nonlocal hwm
"""PCT of current WORKING equity, drawdown-braked, clamped — the copybot's
rule. Sweeps surplus cash to the banked reserve first, so working equity
stays at the level where stakes ~= STAKE_CAP."""
nonlocal hwm, cash, reserve
eq = cash + sum(c for _, c, _, _ in held)
excess = eq - STAKE_CAP / PCT
if excess > 0:
sweep = min(cash, excess)
cash -= sweep; reserve += sweep; eq -= sweep
hwm = max(hwm, eq)
frac = PCT * (DD_FACTOR if eq < DD_THRESHOLD * hwm else 1.0)
return max(STAKE_MIN, min(STAKE_CAP, frac * eq))
@@ -296,16 +306,18 @@ def main():
for w in WALLETS:
t = cache.conv_cutoff(b["size"] for b in cache.get_bets(w["wallet"]) if (b["size"] or 0) > 0)
conv_thr[w["wallet"]] = round(t) if t != float("inf") else 1e12
equity = cash + invested
equity = cash + invested + reserve
out = {
"started": START, "updated": now,
"bank": BANK, "stake": round(cur_stake(), 2), # the NEXT bet's size
"stake_pct": PCT, "event_cap": EVENT_CAP, "hwm": round(hwm, 2),
"stake_pct": PCT, "stake_cap": STAKE_CAP, "event_cap": EVENT_CAP,
"hwm": round(hwm, 2),
"dd_threshold": DD_THRESHOLD, "capped_count": capped,
"max_entry": MAX_ENTRY,
"fee_rate": FEE_RATE, "slip": SLIP, "lag_est_s": LAG_EST_S,
"fees_paid": round(fees_paid, 2),
"equity": round(equity, 2), "liquid": round(cash, 2), "invested": round(invested, 2),
"reserve": round(reserve, 2), # banked profit, never bet
"realized": round(realized, 2), "pnl": round(equity - BANK, 2),
"unreal": round(invested - open_cost, 2),
"resolved_count": len(resolved), "wins": wins, "losses": len(resolved) - wins,
@@ -330,10 +342,10 @@ def main():
json.dump(out, open(os.path.join(HERE, OUT) if not os.path.isabs(OUT) else OUT, "w"),
separators=(",", ":"))
save_slug_cache()
print(f"portfolio: equity ${equity:,.0f} ({(equity-BANK)/BANK*100:+.0f}%) | realized ${realized:+,.0f} "
f"| fees ${fees_paid:,.0f} | next stake ${cur_stake():,.0f} | {len(resolved)} resolved "
f"({wins}W/{len(resolved)-wins}L) | {len(current)} open | {len(missed)} missed "
f"({capped} event-capped) | -> portfolio.json", flush=True)
print(f"portfolio: equity ${equity:,.0f} ({(equity-BANK)/BANK*100:+.0f}%) | banked ${reserve:,.0f} "
f"| realized ${realized:+,.0f} | fees ${fees_paid:,.0f} | next stake ${cur_stake():,.0f} "
f"| {len(resolved)} resolved ({wins}W/{len(resolved)-wins}L) | {len(current)} open "
f"| {len(missed)} missed ({capped} event-capped) | -> portfolio.json", flush=True)
if __name__ == "__main__":