mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-28 00:07:47 +00:00
live retune (user 2026-07-10): caps off, 4% paper-parity, abs guard, pending registry
1. Caps retired: risk block mirrors the paper sentinels; sizing is the paper bot's 4%-of-equity (class_pct 0.04/0.12), floored at the venue's $1 min order (4% of a $22 book is $0.89 — sub-min stakes died at the gate). bankroll rebased to the real $22.28 equity; spend tracker reset (the confusing $35 is gone with the daily cap). 2. Price guard is now ABSOLUTE +0.05 (both books): 0.14→0.15 follows, 0.14→0.20 skips. The relative 5% blocked one-tick moves on cheap in-play books. 3. Pending-order registry: in-play 'delayed' holds are no longer cancelled at 20s — the executor hands them to state.pending_orders with full copy context; the heartbeat resolver adopts the fill whenever it lands (bets/my_pos/cash/ledger, TTL 600s → cancel + honest miss). Recovers Rune-Eaters-class holds (+$7.50 forfeited by the old cancel). 5 stub-client paths pass incl. adopt + expire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+11
-12
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"mode": "live",
|
"mode": "live",
|
||||||
"_comment": "Live-test config, SECRET-FREE and committed. The Fly live worker (wwf-copybot-live, COPYBOT_ROLE=live) reads THIS file and takes secrets from env (LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS + LIVE_CONFIRM to arm). Mac use: cp -> config.live.json and fill live.*. RULES: bankroll_usd MUST equal the actual USDC deposit at arm time (the 1.4 cash-anchor checks state cash vs chain); risk caps are LIVE_ROLLOUT rule 0.6 \u2014 never raise before Phase 6; follow set mirrors copybot.paper.json (Set E) so paper and live books stay comparable.",
|
"_comment": "Live-test config, SECRET-FREE and committed. The Fly live worker (wwf-copybot-live, COPYBOT_ROLE=live) reads THIS file and takes secrets from env (LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS + LIVE_CONFIRM to arm). Mac use: cp -> config.live.json and fill live.*. RULES: bankroll_usd MUST equal actual equity at rebase time (the 1.4 cash-anchor checks state cash vs chain). 2026-07-10 USER decision: hard caps retired \u2014 sizing is paper-parity 4%-of-equity (floored at the venue $1 min order); follow set mirrors copybot.paper.json (Set E) so paper and live books stay comparable.",
|
||||||
"bankroll_usd": 24.73,
|
"bankroll_usd": 22.28,
|
||||||
"bankroll_pct": 0.2022,
|
"bankroll_pct": 0.04,
|
||||||
"price_guard_pct": 0.05,
|
"price_guard_abs": 0.05,
|
||||||
"taker_fee_rate": 0.03,
|
"taker_fee_rate": 0.03,
|
||||||
"discord_webhook": "",
|
"discord_webhook": "",
|
||||||
"follow": {
|
"follow": {
|
||||||
@@ -21,16 +21,16 @@
|
|||||||
"min_entry": 0.0,
|
"min_entry": 0.0,
|
||||||
"max_entry": 0.95,
|
"max_entry": 0.95,
|
||||||
"class_pct": {
|
"class_pct": {
|
||||||
"volume": 0.2022,
|
"volume": 0.04,
|
||||||
"whale": 0.2022
|
"whale": 0.12
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"risk": {
|
"risk": {
|
||||||
"max_trade_usd": 5.0,
|
"max_trade_usd": 1000000.0,
|
||||||
"max_position_usd": 5.0,
|
"max_position_usd": 1000000.0,
|
||||||
"daily_spend_cap_usd": 25.0,
|
"daily_spend_cap_usd": 1000000.0,
|
||||||
"max_total_exposure_usd": 30.0,
|
"max_total_exposure_usd": 1000000.0,
|
||||||
"max_open_positions": 6,
|
"max_open_positions": 1000,
|
||||||
"max_per_event": 0,
|
"max_per_event": 0,
|
||||||
"min_price": 0.01,
|
"min_price": 0.01,
|
||||||
"max_price": 0.95,
|
"max_price": 0.95,
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
"auto_redeem": false,
|
"auto_redeem": false,
|
||||||
"rpc_url": ""
|
"rpc_url": ""
|
||||||
},
|
},
|
||||||
"stake_cap_usd": 24.73,
|
|
||||||
"wallets": [
|
"wallets": [
|
||||||
{
|
{
|
||||||
"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
|
"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
|
||||||
|
|||||||
+130
-22
@@ -286,13 +286,15 @@ class LedgerLiveExecutor:
|
|||||||
return b.balance / 1e6
|
return b.balance / 1e6
|
||||||
|
|
||||||
def _settle_uncertain(self, token_id, side, bal0, price, order_id=None,
|
def _settle_uncertain(self, token_id, side, bal0, price, order_id=None,
|
||||||
deadline_s=20):
|
deadline_s=20, cancel=True):
|
||||||
"""An order may be resting/held at the exchange (in-play 'delayed'
|
"""An order may be resting/held at the exchange (in-play 'delayed'
|
||||||
acceptance, or an exception after posting). Poll it to a terminal
|
acceptance, or an exception after posting). Poll it to a terminal
|
||||||
state, cancel whatever remains, and return (filled, avg_price) from
|
state, cancel whatever remains (unless the caller keeps it alive for
|
||||||
the exchange's own balance diff. INVARIANT (2026-07-10 incident: six
|
the pending registry), and return (filled, avg_price) from the
|
||||||
|
exchange's own balance diff. INVARIANT (2026-07-10 incident: six
|
||||||
in-play acceptances were logged as misses and filled untracked
|
in-play acceptances were logged as misses and filled untracked
|
||||||
minutes later): no order outlives this call untracked."""
|
minutes later): no order outlives this call untracked — it either
|
||||||
|
reports its fill here or is handed to state["pending_orders"]."""
|
||||||
import time
|
import time
|
||||||
px = price
|
px = price
|
||||||
deadline = time.time() + deadline_s
|
deadline = time.time() + deadline_s
|
||||||
@@ -306,16 +308,17 @@ class LedgerLiveExecutor:
|
|||||||
break
|
break
|
||||||
except Exception: # gone from the open view — terminal
|
except Exception: # gone from the open view — terminal
|
||||||
break
|
break
|
||||||
try:
|
if cancel:
|
||||||
if order_id:
|
try:
|
||||||
self.client.cancel_order(order_id=order_id)
|
if order_id:
|
||||||
else: # exception path: sweep the whole token
|
self.client.cancel_order(order_id=order_id)
|
||||||
ids = [o.id for o in self.client.list_open_orders(
|
else: # exception path: sweep the whole token
|
||||||
token_id=str(token_id))]
|
ids = [o.id for o in self.client.list_open_orders(
|
||||||
if ids:
|
token_id=str(token_id))]
|
||||||
self.client.cancel_orders(order_ids=ids)
|
if ids:
|
||||||
except Exception:
|
self.client.cancel_orders(order_ids=ids)
|
||||||
pass # cancel of a just-matched order — fine
|
except Exception:
|
||||||
|
pass # cancel of a just-matched order — fine
|
||||||
try:
|
try:
|
||||||
bal1 = self._shares_held(token_id)
|
bal1 = self._shares_held(token_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -363,10 +366,20 @@ class LedgerLiveExecutor:
|
|||||||
px = usd / filled if filled else price
|
px = usd / filled if filled else price
|
||||||
if filled <= 0:
|
if filled <= 0:
|
||||||
# ACCEPTED with zero matched = in-play 'delayed'/'live' hold, NOT
|
# ACCEPTED with zero matched = in-play 'delayed'/'live' hold, NOT
|
||||||
# a rejection (the 2026-07-10 lesson). Wait it out briefly, then
|
# a rejection (the 2026-07-10 lesson). Wait briefly in-call; if
|
||||||
# cancel-and-measure so the ledger always matches the exchange.
|
# still held, hand the order to the PENDING registry — the
|
||||||
|
# heartbeat resolver adopts the fill when it lands or cancels at
|
||||||
|
# TTL (the 20s cancel-everything version forfeited a Rune-Eaters
|
||||||
|
# hold that filled at +4.5min and paid +$7.50).
|
||||||
filled, px = self._settle_uncertain(token_id, side, bal0, price,
|
filled, px = self._settle_uncertain(token_id, side, bal0, price,
|
||||||
order_id=r.order_id)
|
order_id=r.order_id,
|
||||||
|
deadline_s=8, cancel=False)
|
||||||
|
if filled <= 0:
|
||||||
|
return {"ok": False, "filled_shares": 0.0, "price": price,
|
||||||
|
"pending": {"order_id": r.order_id, "bal0": bal0},
|
||||||
|
"resp": {"order_id": r.order_id, "status": r.status,
|
||||||
|
"note": "in-play hold — pending resolver"},
|
||||||
|
"paper": False}
|
||||||
return {"ok": filled > 0, "filled_shares": filled, "price": px,
|
return {"ok": filled > 0, "filled_shares": filled, "price": px,
|
||||||
"resp": {"order_id": r.order_id, "status": r.status,
|
"resp": {"order_id": r.order_id, "status": r.status,
|
||||||
"making": making, "taking": taking,
|
"making": making, "taking": taking,
|
||||||
@@ -1021,6 +1034,96 @@ class Copybot:
|
|||||||
log(f"baseline: {n} historical trades marked seen · {fresh} fresh trades "
|
log(f"baseline: {n} historical trades marked seen · {fresh} fresh trades "
|
||||||
f"left copyable — only NEW trades from now")
|
f"left copyable — only NEW trades from now")
|
||||||
|
|
||||||
|
def resolve_pendings(self):
|
||||||
|
"""Settle state["pending_orders"] — in-play holds the executor handed
|
||||||
|
off instead of cancelling (2026-07-10 registry). Each pending either
|
||||||
|
ADOPTS its fill (full bookkeeping: spend, position, cash drain,
|
||||||
|
ledger row, bet record) or expires at TTL into a cancel + honest
|
||||||
|
miss. The exchange's balance diff is the fill arbiter, same as the
|
||||||
|
executor's own uncertain path."""
|
||||||
|
st = self.engine.state
|
||||||
|
pend = st.get("pending_orders") or []
|
||||||
|
if not pend or not self.engine.ex.live:
|
||||||
|
return
|
||||||
|
ex = self.engine.ex
|
||||||
|
now = time.time()
|
||||||
|
keep = []
|
||||||
|
for p in pend:
|
||||||
|
tok = p["token"]
|
||||||
|
px, status, matched = p["price"], "gone", 0.0
|
||||||
|
try:
|
||||||
|
o = ex.client.get_order(order_id=p["order_id"])
|
||||||
|
matched = float(o.size_matched or 0)
|
||||||
|
status = o.status
|
||||||
|
if matched > 0:
|
||||||
|
px = float(o.price or px)
|
||||||
|
except Exception:
|
||||||
|
pass # gone from open view — terminal
|
||||||
|
expired = now - p["ts"] > p.get("ttl_s", 600)
|
||||||
|
if status in ("live", "delayed") and not expired and matched <= 0:
|
||||||
|
keep.append(p) # still held — check again next tick
|
||||||
|
continue
|
||||||
|
if status in ("live", "delayed"): # expired: kill the remainder
|
||||||
|
try:
|
||||||
|
ex.client.cancel_order(order_id=p["order_id"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
filled = matched
|
||||||
|
try: # balance diff is the arbiter
|
||||||
|
bal1 = ex._shares_held(tok)
|
||||||
|
diff = (bal1 - p["bal0"]) if p["side"] == "BUY" else (p["bal0"] - bal1)
|
||||||
|
filled = max(filled, diff)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if filled <= 0.01:
|
||||||
|
log(f"pending expired unfilled: {p['outcome']} · {p['title'][:40]}")
|
||||||
|
if p["side"] == "BUY" and not p.get("is_add"):
|
||||||
|
self.engine.record_miss(
|
||||||
|
p["wallet"], tok, p.get("cond"), p["title"], p["outcome"],
|
||||||
|
p["price"], p.get("stake", 0),
|
||||||
|
f"in-play hold expired unfilled ({int(now - p['ts'])}s)")
|
||||||
|
continue
|
||||||
|
spent = filled * px
|
||||||
|
if p["side"] == "BUY":
|
||||||
|
st["spend"]["usd"] += spent
|
||||||
|
mine = st["my_pos"].get(tok)
|
||||||
|
if p.get("is_add") and mine:
|
||||||
|
mine["shares"] += filled
|
||||||
|
mine["cost"] += spent
|
||||||
|
if p.get("cond"):
|
||||||
|
mine.setdefault("cond", p["cond"])
|
||||||
|
else:
|
||||||
|
st["my_pos"][tok] = {
|
||||||
|
"shares": filled, "cost": spent, "title": p["title"],
|
||||||
|
"outcome": p["outcome"], "event": p.get("event"),
|
||||||
|
"wallet": p["wallet"], "cond": p.get("cond")}
|
||||||
|
if p.get("cond"):
|
||||||
|
self.conds[tok] = p["cond"]
|
||||||
|
ex.fills.append({"side": "BUY", "token": tok,
|
||||||
|
"shares": filled, "price": px})
|
||||||
|
synth = {"timestamp": p.get("their_ts"), "price": p["their_price"],
|
||||||
|
"outcome": p["outcome"], "title": p["title"]}
|
||||||
|
for f in self._drain_fills():
|
||||||
|
self._record_lag(p["wallet"], synth, f)
|
||||||
|
log(f"PENDING FILLED · {p['outcome']} · {p['title'][:40]} — "
|
||||||
|
f"buy {filled:.2f} @ {px:.3f} (${spent:.2f}, held "
|
||||||
|
f"{int(now - p['ts'])}s)")
|
||||||
|
else: # SELL adoption: reduce the position
|
||||||
|
mine = st["my_pos"].get(tok)
|
||||||
|
if mine and mine.get("shares"):
|
||||||
|
frac = min(1.0, filled / mine["shares"])
|
||||||
|
mine["cost"] *= (1 - frac)
|
||||||
|
mine["shares"] -= filled
|
||||||
|
if mine["shares"] <= 0.01:
|
||||||
|
st["my_pos"].pop(tok, None)
|
||||||
|
ex.fills.append({"side": "SELL", "token": tok,
|
||||||
|
"shares": filled, "price": px})
|
||||||
|
self._drain_fills()
|
||||||
|
log(f"PENDING EXIT FILLED · {p['outcome']} · {p['title'][:40]} — "
|
||||||
|
f"sold {filled:.2f} @ {px:.3f}")
|
||||||
|
st["pending_orders"] = keep
|
||||||
|
self.engine.persist()
|
||||||
|
|
||||||
_chain_bal = (0.0, None) # (checked_at, usdc) — cached; poll ≤1/min
|
_chain_bal = (0.0, None) # (checked_at, usdc) — cached; poll ≤1/min
|
||||||
|
|
||||||
def chain_cash_gap(self):
|
def chain_cash_gap(self):
|
||||||
@@ -1738,11 +1841,14 @@ def main():
|
|||||||
log(f"copybot · mode: {mode}")
|
log(f"copybot · mode: {mode}")
|
||||||
log(f"on-chain settle fallback: {'ON' if _RPC_URL else 'OFF — set ALCHEMY_RPC_URL'}")
|
log(f"on-chain settle fallback: {'ON' if _RPC_URL else 'OFF — set ALCHEMY_RPC_URL'}")
|
||||||
log(f"watching {len(cfg.get('watchlist', []))} wallets · {filt.describe()}")
|
log(f"watching {len(cfg.get('watchlist', []))} wallets · {filt.describe()}")
|
||||||
log(f"bankroll ${cfg['bankroll_usd']:.0f} @ {cfg['bankroll_pct']:.1%}/entry · "
|
guard = cfg.get("price_guard_abs", cfg.get("price_guard_pct", 0.05))
|
||||||
f"guard {cfg['price_guard_pct']:.0%} · "
|
def _cap(v):
|
||||||
f"caps: ${cfg['risk']['max_trade_usd']:.0f}/trade, "
|
return "off" if v >= 1e5 else f"${v:,.0f}"
|
||||||
f"${cfg['risk']['daily_spend_cap_usd']:.0f}/day, "
|
log(f"bankroll ${cfg['bankroll_usd']:.2f} @ {cfg['bankroll_pct']:.1%}/entry · "
|
||||||
f"${cfg['risk']['max_total_exposure_usd']:.0f} exposure")
|
f"guard +{guard:.2f} abs · "
|
||||||
|
f"caps: {_cap(cfg['risk']['max_trade_usd'])}/trade, "
|
||||||
|
f"{_cap(cfg['risk']['daily_spend_cap_usd'])}/day, "
|
||||||
|
f"{_cap(cfg['risk']['max_total_exposure_usd'])} exposure")
|
||||||
bot.seed()
|
bot.seed()
|
||||||
# boot invariant pass: rebuild any missing bet/conds records and — only
|
# boot invariant pass: rebuild any missing bet/conds records and — only
|
||||||
# here, where no trade is in flight — heal a never-debited orphan's cash
|
# here, where no trade is in flight — heal a never-debited orphan's cash
|
||||||
@@ -1793,6 +1899,7 @@ def main():
|
|||||||
cycle = 0
|
cycle = 0
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
bot.resolve_pendings() # adopt/expire in-play held orders
|
||||||
bot.settle_resolved() # recycle capital at resolution
|
bot.settle_resolved() # recycle capital at resolution
|
||||||
if cycle % 5 == 0:
|
if cycle % 5 == 0:
|
||||||
bot.reconcile_exits()
|
bot.reconcile_exits()
|
||||||
@@ -1829,6 +1936,7 @@ def main():
|
|||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
cycle += 1
|
cycle += 1
|
||||||
try:
|
try:
|
||||||
|
bot.resolve_pendings() # adopt/expire in-play held orders
|
||||||
bot.settle_resolved()
|
bot.settle_resolved()
|
||||||
if cycle % 5 == 0:
|
if cycle % 5 == 0:
|
||||||
bot.reconcile_exits()
|
bot.reconcile_exits()
|
||||||
|
|||||||
+6237
-6236
File diff suppressed because it is too large
Load Diff
+50
-10
@@ -58,7 +58,10 @@ DEFAULT_CONFIG = {
|
|||||||
# past cap/bankroll_pct — surplus cash is SWEPT to
|
# past cap/bankroll_pct — surplus cash is SWEPT to
|
||||||
# state["reserve"], a banked pot that never bets
|
# state["reserve"], a banked pot that never bets
|
||||||
# (profit ratchet + keeps fills inside book depth)
|
# (profit ratchet + keeps fills inside book depth)
|
||||||
"price_guard_pct": 0.05, # skip if price moved >5% from their fill
|
"price_guard_abs": 0.05, # skip if price moved >5 POINTS above their
|
||||||
|
# fill (absolute, 2026-07-10: 0.14→0.15 must
|
||||||
|
# follow; relative % blocked 1-tick moves on
|
||||||
|
# cheap in-play books)
|
||||||
"risk": {
|
"risk": {
|
||||||
"max_trade_usd": 50.0, # hard ceiling on any single copy
|
"max_trade_usd": 50.0, # hard ceiling on any single copy
|
||||||
"max_position_usd": 40.0, # hard ceiling on total cost in one market
|
"max_position_usd": 40.0, # hard ceiling on total cost in one market
|
||||||
@@ -307,7 +310,10 @@ class CopyTrader:
|
|||||||
stake = frac * eq
|
stake = frac * eq
|
||||||
if their and stake > their:
|
if their and stake > their:
|
||||||
stake = their
|
stake = their
|
||||||
return stake
|
# venue floor: the CLOB rejects sub-$1 orders, so a small book's pct
|
||||||
|
# stake must round UP to the minimum or every copy dies at the gate
|
||||||
|
# (4% of the $22 live book = $0.89 — 2026-07-10 paper-parity retune)
|
||||||
|
return max(stake, self.risk.get("min_order_usd", 1.0))
|
||||||
|
|
||||||
def record_miss(self, wallet, token, cond, title, outcome, price, want, reason):
|
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 —
|
"""A bet the strategy WOULD have copied but the book couldn't take —
|
||||||
@@ -377,7 +383,8 @@ class CopyTrader:
|
|||||||
if side == "BUY":
|
if side == "BUY":
|
||||||
self._handle_their_buy(wallet, token, their_size, their_price,
|
self._handle_their_buy(wallet, token, their_size, their_price,
|
||||||
label, title, outcome, event=event_key(t),
|
label, title, outcome, event=event_key(t),
|
||||||
cond=t.get("conditionId"))
|
cond=t.get("conditionId"),
|
||||||
|
their_ts=t.get("timestamp"))
|
||||||
their_book[token] = their_prev + their_size
|
their_book[token] = their_prev + their_size
|
||||||
elif side == "SELL":
|
elif side == "SELL":
|
||||||
self._handle_their_sell(token, their_size, their_prev, label)
|
self._handle_their_sell(token, their_size, their_prev, label)
|
||||||
@@ -398,13 +405,18 @@ class CopyTrader:
|
|||||||
# ASYMMETRIC by rule: a better price than the sharp paid is never blocked
|
# ASYMMETRIC by rule: a better price than the sharp paid is never blocked
|
||||||
# (paying less for the same outcome is strictly better odds — the guard
|
# (paying less for the same outcome is strictly better odds — the guard
|
||||||
# once skipped a 0.70→0.51 improvement that went on to win). Only adverse
|
# once skipped a 0.70→0.51 improvement that went on to win). Only adverse
|
||||||
# drift — chasing the price UP — is gated by price_guard_pct.
|
# drift — chasing the price UP — is gated, in ABSOLUTE points: the old
|
||||||
|
# relative 5% blocked one-tick moves on cheap in-play books (0.14→0.15
|
||||||
|
# is +7% relative but the same bet; 0.14→0.19 is where the edge is gone).
|
||||||
if current <= their_price:
|
if current <= their_price:
|
||||||
return True
|
return True
|
||||||
return (current - their_price) / their_price <= self.cfg["price_guard_pct"]
|
guard = self.cfg.get("price_guard_abs",
|
||||||
|
self.cfg.get("price_guard_pct", 0.05))
|
||||||
|
return (current - their_price) <= guard
|
||||||
|
|
||||||
def _handle_their_buy(self, wallet, token, their_size, their_price,
|
def _handle_their_buy(self, wallet, token, their_size, their_price,
|
||||||
label, title, outcome, event=None, cond=None):
|
label, title, outcome, event=None, cond=None,
|
||||||
|
their_ts=None):
|
||||||
mine = self.state["my_pos"].get(token)
|
mine = self.state["my_pos"].get(token)
|
||||||
is_add = mine is not None
|
is_add = mine is not None
|
||||||
# the signal's position in this token BEFORE this trade — the their-bet
|
# the signal's position in this token BEFORE this trade — the their-bet
|
||||||
@@ -447,7 +459,8 @@ class CopyTrader:
|
|||||||
return
|
return
|
||||||
if not self._price_guard_ok(price, their_price):
|
if not self._price_guard_ok(price, their_price):
|
||||||
self.log(f"BUY {label} — skip (price {price:.3f} vs their "
|
self.log(f"BUY {label} — skip (price {price:.3f} vs their "
|
||||||
f"{their_price:.3f}, >{self.cfg['price_guard_pct']:.0%})")
|
f"{their_price:.3f}, moved >"
|
||||||
|
f"{self.cfg.get('price_guard_abs', 0.05):.2f} abs)")
|
||||||
self.record_miss(wallet, token, cond, title, outcome, price,
|
self.record_miss(wallet, token, cond, title, outcome, price,
|
||||||
self.stake_usd(wallet),
|
self.stake_usd(wallet),
|
||||||
f"price moved {their_price:.2f}→{price:.2f}")
|
f"price moved {their_price:.2f}→{price:.2f}")
|
||||||
@@ -484,6 +497,21 @@ class CopyTrader:
|
|||||||
shares = allowed / price
|
shares = allowed / price
|
||||||
res = self.ex.buy(token, shares, price, {"title": title})
|
res = self.ex.buy(token, shares, price, {"title": title})
|
||||||
if not res["ok"]:
|
if not res["ok"]:
|
||||||
|
# in-play books ACCEPT orders with a delayed hold — the executor
|
||||||
|
# reports those as pending (order id + pre-order balance) instead
|
||||||
|
# of failed. Park the full copy context; the heartbeat resolver
|
||||||
|
# adopts the fill when it lands or converts to a miss at TTL.
|
||||||
|
if res.get("pending"):
|
||||||
|
self.state.setdefault("pending_orders", []).append({
|
||||||
|
**res["pending"], "token": token, "side": "BUY",
|
||||||
|
"wallet": wallet, "title": title, "outcome": outcome,
|
||||||
|
"event": event, "cond": cond, "their_price": their_price,
|
||||||
|
"their_ts": their_ts, "price": price, "is_add": is_add,
|
||||||
|
"stake": allowed, "ts": time.time(), "ttl_s": 600})
|
||||||
|
self.log(f"{kind} {label} — PENDING (in-play hold, "
|
||||||
|
f"order {str(res['pending'].get('order_id'))[:14]}…)")
|
||||||
|
self.persist()
|
||||||
|
return
|
||||||
self.log(f"{kind} {label} — ORDER FAILED: {res.get('resp')}")
|
self.log(f"{kind} {label} — ORDER FAILED: {res.get('resp')}")
|
||||||
if not is_add: # a rejected OPEN is a missed bet
|
if not is_add: # a rejected OPEN is a missed bet
|
||||||
self.record_miss(wallet, token, cond, title, outcome, price,
|
self.record_miss(wallet, token, cond, title, outcome, price,
|
||||||
@@ -525,6 +553,17 @@ class CopyTrader:
|
|||||||
return
|
return
|
||||||
res = self.ex.sell(token, sell_shares, price, {})
|
res = self.ex.sell(token, sell_shares, price, {})
|
||||||
if not res["ok"]:
|
if not res["ok"]:
|
||||||
|
if res.get("pending"): # in-play hold — resolver adopts
|
||||||
|
self.state.setdefault("pending_orders", []).append({
|
||||||
|
**res["pending"], "token": token, "side": "SELL",
|
||||||
|
"wallet": mine.get("wallet", ""), "title": mine.get("title", ""),
|
||||||
|
"outcome": mine.get("outcome", ""), "event": mine.get("event"),
|
||||||
|
"cond": mine.get("cond"), "their_price": price,
|
||||||
|
"their_ts": None, "price": price, "is_add": False,
|
||||||
|
"stake": sell_shares * price, "ts": time.time(), "ttl_s": 600})
|
||||||
|
self.log(f"EXIT {label} — PENDING (in-play hold)")
|
||||||
|
self.persist()
|
||||||
|
return
|
||||||
self.log(f"EXIT {label} — ORDER FAILED: {res.get('resp')}")
|
self.log(f"EXIT {label} — ORDER FAILED: {res.get('resp')}")
|
||||||
return
|
return
|
||||||
proceeds = res["filled_shares"] * res["price"]
|
proceeds = res["filled_shares"] * res["price"]
|
||||||
@@ -605,9 +644,10 @@ class CopyTrader:
|
|||||||
def confirm_live(cfg):
|
def confirm_live(cfg):
|
||||||
print("\n" + "=" * 64)
|
print("\n" + "=" * 64)
|
||||||
print(" LIVE MODE — this will place REAL orders with REAL money.")
|
print(" LIVE MODE — this will place REAL orders with REAL money.")
|
||||||
print(f" Bankroll ${cfg['bankroll_usd']:.0f} · {cfg['bankroll_pct']:.1%}/entry"
|
mt, dc = cfg['risk']['max_trade_usd'], cfg['risk']['daily_spend_cap_usd']
|
||||||
f" · max ${cfg['risk']['max_trade_usd']:.0f}/trade"
|
print(f" Bankroll ${cfg['bankroll_usd']:.2f} · {cfg['bankroll_pct']:.1%}/entry"
|
||||||
f" · daily cap ${cfg['risk']['daily_spend_cap_usd']:.0f}")
|
f" · max {'off' if mt >= 1e5 else '$%.0f' % mt}/trade"
|
||||||
|
f" · daily cap {'off' if dc >= 1e5 else '$%.0f' % dc}")
|
||||||
print(f" Watching {len(cfg['watchlist'])} wallets.")
|
print(f" Watching {len(cfg['watchlist'])} wallets.")
|
||||||
print("=" * 64)
|
print("=" * 64)
|
||||||
# Headless arm (Fly live worker): the USER types the exact phrase into
|
# Headless arm (Fly live worker): the USER types the exact phrase into
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"mode": "paper",
|
"mode": "paper",
|
||||||
"bankroll_usd": 1000.0,
|
"bankroll_usd": 1000.0,
|
||||||
"bankroll_pct": 0.04,
|
"bankroll_pct": 0.04,
|
||||||
"price_guard_pct": 0.05,
|
|
||||||
"wallets": [
|
"wallets": [
|
||||||
{
|
{
|
||||||
"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
|
"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
|
||||||
@@ -67,5 +66,6 @@
|
|||||||
"max_price": 0.99,
|
"max_price": 0.99,
|
||||||
"min_order_usd": 5.0,
|
"min_order_usd": 5.0,
|
||||||
"max_per_event": 0
|
"max_per_event": 0
|
||||||
}
|
},
|
||||||
|
"price_guard_abs": 0.05
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user