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:
@@ -38,3 +38,4 @@ live/scored.json
|
||||
live/*_scored.json
|
||||
live/watch_prejune*.json
|
||||
live/history/
|
||||
live/slug_cache.json
|
||||
|
||||
+52
-6
@@ -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} "
|
||||
|
||||
+5
-2
@@ -377,7 +377,10 @@ class Copybot:
|
||||
lag = st.get("lag", {})
|
||||
feed = {
|
||||
"mode": "live" if self.engine.ex.live else "paper",
|
||||
"bankroll": bank, "stake": round(bank * self.cfg["bankroll_pct"], 2),
|
||||
"bankroll": bank, "stake": round(self.engine.stake_usd(), 2),
|
||||
"stake_pct": self.cfg["bankroll_pct"],
|
||||
"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),
|
||||
"fees_paid": round(st.get("fees_paid", 0.0), 2),
|
||||
@@ -468,7 +471,7 @@ class Copybot:
|
||||
|
||||
def summary(self, cycle):
|
||||
bank = self.cfg["bankroll_usd"]
|
||||
stake = bank * self.cfg["bankroll_pct"]
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"mode": "paper",
|
||||
"bankroll_usd": 1000.0,
|
||||
"bankroll_pct": 0.05,
|
||||
"bankroll_pct": 0.04,
|
||||
"price_guard_pct": 0.05,
|
||||
"watchlist": [
|
||||
"0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb",
|
||||
@@ -40,13 +40,14 @@
|
||||
"max_entry": 1.0
|
||||
},
|
||||
"risk": {
|
||||
"max_trade_usd": 50.0,
|
||||
"max_position_usd": 50.0,
|
||||
"max_trade_usd": 150.0,
|
||||
"max_position_usd": 150.0,
|
||||
"daily_spend_cap_usd": 1000000.0,
|
||||
"max_total_exposure_usd": 1000000.0,
|
||||
"max_open_positions": 1000,
|
||||
"min_price": 0.01,
|
||||
"max_price": 0.99,
|
||||
"min_order_usd": 5.0
|
||||
"min_order_usd": 5.0,
|
||||
"max_per_event": 2
|
||||
}
|
||||
}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+99
-28
@@ -10,20 +10,24 @@ RECYCLES correctly (cash frees at the true resolution moment). Output -> portfol
|
||||
which the dashboard reads in one request.
|
||||
|
||||
Model: a $1,000 account that mirrors each followed wallet's CONVICTION bets (top-20%
|
||||
stake) at a flat $50, held to resolution (the cache has no sell events, which is the
|
||||
right model for the hold-to-resolution wallets we follow). Entries pay the Polymarket
|
||||
taker fee and a lag-slippage price haircut (see FEE_RATE / SLIP / LAG_EST_S) so the
|
||||
book models what a real copier nets, not the idealized zero-cost mirror. One position
|
||||
per market (first wallet to enter wins the slot); when capital is fully deployed a bet
|
||||
is MISSED.
|
||||
stake), held to resolution (the cache has no sell events, which is the right model for
|
||||
the hold-to-resolution wallets we follow). Sizing is DYNAMIC — each bet stakes PCT of
|
||||
current equity (Kelly-style compounding), halved in a >20% drawdown, capped at
|
||||
EVENT_CAP concurrent bets per real-world event — and entries pay the Polymarket taker
|
||||
fee plus a lag-slippage price haircut (FEE_RATE / SLIP / LAG_EST_S), so the book
|
||||
models what a real copier nets, not the idealized zero-cost mirror. One position per
|
||||
market (first wallet to enter wins the slot); when capital is fully deployed a bet is
|
||||
MISSED.
|
||||
Resolved history + realized P&L come from the cache; currently-open bets come from a
|
||||
small live /positions pull so the page can still show what's in flight.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import time
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import cache
|
||||
import smart_money as sm
|
||||
@@ -32,14 +36,25 @@ _SSL = ssl._create_unverified_context()
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
BANK = 1000.0
|
||||
STAKE = 50.0
|
||||
START = time.mktime(time.strptime("2026-06-01", "%Y-%m-%d")) # backfilled: replay from June 1
|
||||
GAMMA = "https://gamma-api.polymarket.com"
|
||||
|
||||
# ---- dynamic sizing (mirrors the live copybot) ------------------------------
|
||||
# Each new bet stakes PCT of CURRENT equity (cash + open cost basis) so the book
|
||||
# compounds in both directions; the stake is halved while equity sits below
|
||||
# DD_THRESHOLD of its high-water mark, and clamped to [STAKE_MIN, STAKE_CAP].
|
||||
# EVENT_CAP limits concurrent bets whose markets belong to the same real-world
|
||||
# event — a game's markets settle together, so N bets on one match are one
|
||||
# correlated bet, not N diversified ones.
|
||||
PCT = 0.04
|
||||
STAKE_MIN, STAKE_CAP = 5.0, 150.0
|
||||
EVENT_CAP = 2
|
||||
DD_THRESHOLD, DD_FACTOR = 0.80, 0.5
|
||||
|
||||
# ---- realism model (matches the live copybot) -------------------------------
|
||||
# Taker fee (Polymarket V2, since 2026-03-30): fee = shares·rate·p·(1−p); for a
|
||||
# flat-$STAKE buy that's STAKE·rate·(1−p). Sports 0.03 — the follow set's
|
||||
# category. Redeeming at resolution is fee-free, so only entries pay here
|
||||
# $stake buy that's stake·rate·(1−p). Sports 0.03 — the follow set's category.
|
||||
# Redeeming at resolution is fee-free, so only entries pay here
|
||||
# (hold-to-resolution model, no mirrored exits).
|
||||
FEE_RATE = 0.03
|
||||
# Copy lag: we enter LAG_EST_S after the wallet does, at a slightly worse price.
|
||||
@@ -57,17 +72,26 @@ WALLETS = [
|
||||
]
|
||||
|
||||
|
||||
def entry_model(p):
|
||||
"""(effective entry price, entry fee, total cash cost) of a flat-$STAKE copy:
|
||||
def entry_model(p, stake):
|
||||
"""(effective entry price, entry fee, total cash cost) of a $stake copy:
|
||||
price worsened by the lag-slippage haircut, taker fee on top of the stake."""
|
||||
p_eff = min(0.999, p * (1 + SLIP))
|
||||
fee = STAKE * FEE_RATE * (1 - p_eff)
|
||||
return p_eff, fee, STAKE + fee
|
||||
fee = stake * FEE_RATE * (1 - p_eff)
|
||||
return p_eff, fee, stake + fee
|
||||
|
||||
_MKT = {}
|
||||
_SLUG_CACHE = os.path.join(HERE, "slug_cache.json")
|
||||
try:
|
||||
_MKT.update(json.load(open(_SLUG_CACHE)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def market_meta(cond):
|
||||
"""Market title for display, from the CLOB market endpoint (gamma's condition_ids
|
||||
filter returns nothing for resolved markets) — cached."""
|
||||
"""Market title + slug from the CLOB market endpoint (gamma's condition_ids
|
||||
filter returns nothing for resolved markets) — cached in-process AND on disk
|
||||
(slug_cache.json), since the event cap needs a slug for every replayed market,
|
||||
not just the top-60 displayed."""
|
||||
if cond not in _MKT:
|
||||
try:
|
||||
r = urllib.request.urlopen(urllib.request.Request(
|
||||
@@ -76,10 +100,26 @@ def market_meta(cond):
|
||||
m = json.loads(r.read())
|
||||
_MKT[cond] = {"title": m.get("question") or "", "slug": m.get("market_slug") or ""}
|
||||
except Exception:
|
||||
_MKT[cond] = {"title": "", "slug": ""}
|
||||
return {"title": "", "slug": ""} # transient failure — don't cache
|
||||
return _MKT[cond]
|
||||
|
||||
|
||||
def save_slug_cache():
|
||||
try:
|
||||
json.dump({c: v for c, v in _MKT.items() if v.get("slug")},
|
||||
open(_SLUG_CACHE, "w"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def event_key(slug):
|
||||
"""Correlation-group id: Polymarket sub-splits one game across slugs
|
||||
(`…-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."""
|
||||
m = re.match(r"(.*?\d{4}-\d{2}-\d{2})", slug or "")
|
||||
return m.group(1) if m else (slug or None)
|
||||
|
||||
|
||||
def conviction_bets():
|
||||
"""Every followed wallet's resolved conviction bets from the cache, with entry time."""
|
||||
out = []
|
||||
@@ -145,14 +185,29 @@ def main():
|
||||
b["kind"] = "open"; by_mkt[b["cond"]] = b
|
||||
stream = sorted(by_mkt.values(), key=lambda b: b["entry_t"])
|
||||
|
||||
# prefetch every replayed market's slug (threaded; disk-cached) so the
|
||||
# event-correlation cap can group markets by real-world event
|
||||
with ThreadPoolExecutor(max_workers=8) as ex:
|
||||
list(ex.map(market_meta, {b["cond"] for b in stream}))
|
||||
|
||||
cash = BANK
|
||||
realized = 0.0
|
||||
fees_paid = 0.0
|
||||
hwm = BANK
|
||||
capped = 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,
|
||||
"invested": 0.0, "realized": 0.0} for w in WALLETS}
|
||||
resolved, current, missed = [], [], []
|
||||
|
||||
def cur_stake():
|
||||
"""PCT of current equity, drawdown-braked, clamped — the copybot's rule."""
|
||||
nonlocal hwm
|
||||
eq = cash + sum(c for _, c, _, _ in held)
|
||||
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))
|
||||
|
||||
def free(upto):
|
||||
nonlocal cash, realized
|
||||
keep = []
|
||||
@@ -167,16 +222,26 @@ def main():
|
||||
|
||||
for b in stream:
|
||||
free(b["entry_t"])
|
||||
p_eff, fee, cost = entry_model(b["p"])
|
||||
stake = cur_stake()
|
||||
b["stake"] = round(stake, 2)
|
||||
b["event"] = event_key(market_meta(b["cond"])["slug"])
|
||||
# correlation cap: skip a bet when we already hold EVENT_CAP positions on
|
||||
# the same real-world event (deliberate risk skip, tallied separately)
|
||||
if b["event"] and sum(1 for _, _, _, r in held
|
||||
if r.get("event") == b["event"]) >= EVENT_CAP:
|
||||
b["capped"] = True; capped += 1
|
||||
missed.append(b)
|
||||
continue
|
||||
p_eff, fee, cost = entry_model(b["p"], stake)
|
||||
if cash >= cost:
|
||||
cash -= cost; fees_paid += fee; perW[b["wallet"]]["bets"] += 1
|
||||
shares = STAKE / p_eff # lag-adjusted entry price
|
||||
shares = stake / p_eff # lag-adjusted entry price
|
||||
if b["kind"] == "res":
|
||||
payoff = shares * (1.0 if b["won"] else 0.0) # redeem is fee-free
|
||||
held.append((b["res_t"] or now, cost, payoff, b))
|
||||
else: # currently open -> mark to market, no free yet
|
||||
held.append((None, cost, 0.0, b))
|
||||
b["val"] = shares * b["cur"]; b["stake"] = STAKE
|
||||
b["val"] = shares * b["cur"]
|
||||
else:
|
||||
missed.append(b)
|
||||
free(now)
|
||||
@@ -200,10 +265,11 @@ def main():
|
||||
# indexing m["won"] here used to KeyError and kill the whole portfolio step
|
||||
# the first time capital ran out while a followed wallet had a live position.
|
||||
def hypo_pnl(m):
|
||||
p_eff, fee, cost = entry_model(m["p"])
|
||||
stake = m.get("stake") or STAKE_MIN
|
||||
p_eff, fee, cost = entry_model(m["p"], stake)
|
||||
if "won" in m:
|
||||
return (STAKE / p_eff) - cost if m["won"] else -cost
|
||||
return STAKE * (m.get("cur", p_eff) / p_eff) - cost
|
||||
return (stake / p_eff) - cost if m["won"] else -cost
|
||||
return stake * (m.get("cur", p_eff) / p_eff) - cost
|
||||
|
||||
missed.sort(key=lambda m: m.get("res_t") or 0, reverse=True)
|
||||
for m in missed[:60]:
|
||||
@@ -219,7 +285,9 @@ def main():
|
||||
equity = cash + invested
|
||||
out = {
|
||||
"started": START, "updated": now,
|
||||
"bank": BANK, "stake": STAKE,
|
||||
"bank": BANK, "stake": round(cur_stake(), 2), # the NEXT bet's size
|
||||
"stake_pct": PCT, "event_cap": EVENT_CAP, "hwm": round(hwm, 2),
|
||||
"dd_threshold": DD_THRESHOLD, "capped_count": capped,
|
||||
"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),
|
||||
@@ -232,20 +300,23 @@ def main():
|
||||
"conv_thr": conv_thr.get(v["wallet"], 1e12)}
|
||||
for v in perW.values()],
|
||||
"current": [{"title": c.get("title", ""), "name": c["name"], "outcome": c.get("outcome", ""),
|
||||
"stake": STAKE, "val": round(c["val"], 2), "pnl": round(c["pnl"], 2),
|
||||
"stake": c.get("stake"), "val": round(c["val"], 2), "pnl": round(c["pnl"], 2),
|
||||
"end": c.get("end")} for c in sorted(current, key=lambda c: c["entry_t"])],
|
||||
"resolved": [{"title": r.get("title", ""), "name": r["name"], "won": r["won"],
|
||||
"stake": STAKE, "pnl": round(r["pnl"], 2), "date": r.get("res_t")}
|
||||
"stake": r.get("stake"), "pnl": round(r["pnl"], 2), "date": r.get("res_t")}
|
||||
for r in resolved[:60]],
|
||||
"missed": [{"title": m.get("title", ""), "name": m["name"], "won": m.get("won"),
|
||||
"stake": STAKE, "pnl": round(m["pnl"], 2), "date": m.get("res_t")}
|
||||
"stake": m.get("stake"), "capped": bool(m.get("capped")),
|
||||
"pnl": round(m["pnl"], 2), "date": m.get("res_t")}
|
||||
for m in missed[:60]],
|
||||
"missed_pnl": round(sum(hypo_pnl(m) for m in missed), 2),
|
||||
}
|
||||
json.dump(out, open(os.path.join(HERE, "portfolio.json"), "w"), separators=(",", ":"))
|
||||
save_slug_cache()
|
||||
print(f"portfolio: equity ${equity:,.0f} ({(equity-BANK)/BANK*100:+.0f}%) | realized ${realized:+,.0f} "
|
||||
f"| fees ${fees_paid:,.0f} | {len(resolved)} resolved ({wins}W/{len(resolved)-wins}L) "
|
||||
f"| {len(current)} open | {len(missed)} missed | -> portfolio.json", flush=True)
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user