copybot: sold legs now realize on the bet record, not just cash

Mirror-exits were correct at book level (cash/fees) but invisible per bet:
a fully mirror-sold bet went to 'closed' with pnl null, and a TRIMmed bet
that later resolved subtracted its ORIGINAL cost+fee while ignoring the
sold leg's proceeds — understating whole-bet pnl on the feed.

* _drain_fills links every SELL fill to its bet record
  (sold_shares / sold_proceeds, net of taker fee)
* write_feed: full mirror-sell -> pnl = sold_proceeds − cost − entry fee,
  plus exit_price; dashboard's existing 'mirror-sold with recorded P&L'
  branch picks it up
* settle_resolved: pnl = resolution proceeds + sold leg − original cost −
  entry fee (pos.cost is trim-reduced; bet.cost is the original)

Sandbox-tested: TRIM cash+record, TRIM->WIN whole-bet truth, full EXIT
feed reconcile all agree to the cent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-05 21:58:06 -04:00
parent 034e128281
commit e07ff3d305
+20 -1
View File
@@ -329,6 +329,14 @@ class Copybot:
self.engine.state["fees_paid"] = self.engine.state.get("fees_paid", 0.0) + fee
if f["side"] == "BUY":
buys.append(f)
else:
# link the mirror-exit to its bet record so the sold leg shows
# up in per-bet P&L (feed) — cash above is already correct
b = self.engine.state.get("bets", {}).get(f["token"])
if b:
b["sold_shares"] = round(b.get("sold_shares", 0) + f["shares"], 2)
b["sold_proceeds"] = round(b.get("sold_proceeds", 0)
+ f["shares"] * f["price"] - fee, 2)
ex.fills.clear()
return buys
@@ -401,6 +409,12 @@ class Copybot:
if b["status"] == "open" and tok not in mp:
b["status"] = "closed"
b["settled"] = b["settled"] or int(time.time())
# fully mirror-sold: realize the sold leg on the bet record
# (cash was already credited fill-by-fill in _drain_fills)
if b.get("sold_proceeds") is not None and b.get("pnl") is None:
b["pnl"] = round(b["sold_proceeds"] - b["cost"] - (b.get("fee") or 0), 2)
if b.get("sold_shares"):
b["exit_price"] = round(b["sold_proceeds"] / b["sold_shares"], 4)
bank = self.cfg["bankroll_usd"]
exp = self.engine.open_exposure()
cash = st.get("cash", bank)
@@ -594,7 +608,12 @@ class Copybot:
proceeds = pos["shares"] * wp # redeem is fee-free on-chain
b = self.engine.state.get("bets", {}).get(token)
fee_in = (b or {}).get("fee") or 0 # entry taker fee, already off cash
pnl = proceeds - pos["cost"] - fee_in
# a TRIMmed bet realized part of itself pre-resolution: count the
# sold leg and charge the ORIGINAL cost (pos["cost"] was reduced
# proportionally at each trim), so per-bet pnl = whole-bet truth
sold = (b or {}).get("sold_proceeds") or 0
base_cost = b["cost"] if b else pos["cost"]
pnl = proceeds + sold - base_cost - fee_in
self.engine.state["cash"] += proceeds # recycle freed capital
if b:
b.update(status=("won" if wp >= 0.5 else "lost"),