dust sweep: daily reclaim of untracked exit residue — sells booked as adjustment, never P&L

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-16 23:26:12 -04:00
parent 86ba267156
commit afaf4e461f
2 changed files with 139 additions and 1 deletions
+64 -1
View File
@@ -62,7 +62,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from copytrade import ( # the execution engine (sizing, gates, executors)
CopyTrader, PaperExecutor, LiveExecutor, DEFAULT_CONFIG,
load_json, save_json, new_state, recent_trades, confirm_live,
load_json, save_json, new_state, recent_trades, confirm_live, clob_price,
)
import smart_money as sm # noqa: E402
from smart_money import SSL_CTX # noqa: E402
@@ -1640,6 +1640,68 @@ class Copybot:
st["exit_retries"] = keep
self.engine.persist()
DUST_MIN_USD = 0.10 # below this a sweep sell isn't worth the fee
DUST_WALLET = "0x455e252e45Ee46d6C4cc1c8fAdD3899d68f245a1" # deposit wallet
def sweep_dust(self, cycle):
"""Exit-dust reclaimer (2026-07-17): proportional mirror-exits leave
sub-share remainders on chain after the book closes the bet (found by
the platform-vs-book reconcile: 4 residues worth ~$1.90). Shortly
after boot and ~daily, market-sell any LIVE-market wallet holding the
book does NOT track and credit the proceeds as a documented
ADJUSTMENT — recovery of already-settled bets, never new realized P&L
(the ledger invariant needs the cash move in exactly one term).
Never touches my_pos / pending / retry tokens; a failed sell (dust
books FAK no-match, sub-$1 rejects) just waits for the next pass."""
if not getattr(self.engine.ex, "live", False):
return
if cycle != 5 and cycle % 1440 != 0:
return
pos = sm.get_json("/positions", {"user": self.DUST_WALLET,
"sizeThreshold": 0}) or []
st = self.engine.state
skip = (set(st["my_pos"])
| {p.get("token") for p in st.get("pending_orders", [])}
| {r.get("token") for r in st.get("exit_retries", [])})
recovered, sold, tried = 0.0, [], 0
for p in pos:
tok, val = p.get("asset"), p.get("currentValue") or 0
cur, shares = p.get("curPrice") or 0, p.get("size") or 0
if (not tok or tok in skip or val < self.DUST_MIN_USD
or p.get("redeemable") or not 0.01 <= cur <= 0.99
or shares <= 0):
continue
if tried >= 10:
break
tried += 1
quote = clob_price(tok, "sell")
if not quote:
continue
with self.lock:
before = st["cash"]
r = self.engine.ex.sell(tok, shares, quote,
{"title": p.get("title") or "dust"})
self._drain_fills()
delta = st["cash"] - before
if r.get("ok") and delta > 0:
recovered += delta
sold.append(f"{shares:.2f}sh @ {r['price']:.3f} "
f"{str(p.get('title'))[:36]}")
log(f"DUST SWEEP +${delta:.2f} · {sold[-1]}")
else:
log(f"dust sweep skip ({str(r.get('resp'))[:50]}) · "
f"{str(p.get('title'))[:40]}")
if recovered > 0:
st.setdefault("adjustments", []).append({
"ts": int(time.time()), "amount": round(recovered, 6),
"note": f"dust sweep: {len(sold)} residual holdings sold "
"(recovery of closed bets, not P&L)"})
self.engine.persist()
self.engine.alert(
f"dust sweep recovered ${recovered:.2f} ({len(sold)} holdings)",
discord_text=(f"🧹 **DUST SWEEP** [LIVE] +${recovered:.2f}\n"
+ "\n".join(sold[:8])))
_chain_bal = (0.0, None) # (checked_at, usdc) — cached; poll ≤1/min
def chain_cash_gap(self):
@@ -2534,6 +2596,7 @@ def main():
bot.resolve_pendings() # adopt/expire in-play held orders
bot.retry_stuck_exits() # LIVE_ROLLOUT 1.6
bot.settle_resolved()
bot.sweep_dust(cycle) # reclaim untracked exit residue (live)
if cycle % 5 == 0:
bot.reconcile_exits()
bot.reconcile_entries()
+75
View File
@@ -0,0 +1,75 @@
"""Stub test for Copybot.sweep_dust (2026-07-17): sells only untracked
live-market residue, books an adjustment (not P&L), skips my_pos/pending/
floor/redeemable. Run: python3 tests/test_dust_sweep.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import copybot # noqa: E402
class Ex:
live = True
def __init__(self):
self.fills, self.sold = [], []
def sell(self, tok, shares, price, meta):
self.sold.append(tok)
self.fills.append({"side": "SELL", "token": tok, "shares": shares,
"price": price})
return {"ok": True, "filled_shares": shares, "price": price}
class Eng:
def __init__(self, st):
self.state, self.ex, self.alerts = st, Ex(), []
def persist(self):
pass
def alert(self, m, discord_text=None):
self.alerts.append(m)
st = {"my_pos": {"HELD": {}}, "pending_orders": [{"token": "PEND"}],
"exit_retries": [], "cash": 10.0, "adjustments": [], "bets": {}}
bot = copybot.Copybot.__new__(copybot.Copybot)
bot.engine = Eng(st)
bot.fee_rate = 0.03
import threading
bot.lock = threading.Lock()
POS = [
{"asset": "HELD", "currentValue": 5.0, "curPrice": 0.5, "size": 10}, # book-open
{"asset": "PEND", "currentValue": 5.0, "curPrice": 0.5, "size": 10}, # pending
{"asset": "TINY", "currentValue": 0.05, "curPrice": 0.5, "size": 0.1}, # under floor
{"asset": "DEAD", "currentValue": 0.0, "curPrice": 0.0, "size": 2,
"redeemable": True}, # worthless
{"asset": "DUST", "currentValue": 0.37, "curPrice": 0.37, "size": 1.0,
"title": "Team Yandex dust"}, # sweep me
]
copybot.sm.get_json = lambda path, params=None: POS
copybot.clob_price = lambda tok, side: 0.37
bot.sweep_dust(cycle=5)
fails = []
if bot.engine.ex.sold != ["DUST"]:
fails.append(f"sold wrong set: {bot.engine.ex.sold}")
fee = copybot.taker_fee(1.0, 0.37, 0.03)
want_cash = 10.0 + 0.37 - fee
if abs(st["cash"] - want_cash) > 1e-9:
fails.append(f"cash {st['cash']} != {want_cash}")
if not (st["adjustments"] and abs(st["adjustments"][0]["amount"] - (0.37 - fee)) < 1e-9):
fails.append(f"adjustment wrong: {st['adjustments']}")
# realized invariant: cash delta fully covered by the adjustment
if not bot.engine.alerts:
fails.append("no alert sent")
# off-schedule cycles do nothing
bot.sweep_dust(cycle=6)
if len(bot.engine.ex.sold) != 1:
fails.append("swept on an off-schedule cycle")
print("FAILURES:", fails or "none")
sys.exit(1 if fails else 0)