#!/usr/bin/env python3 """Backtest kch123 copy-trading from full trade history""" import json from collections import defaultdict from datetime import datetime with open("kch123-full-trades.json") as f: trades = json.load(f) print(f"Total trade records: {len(trades)}") # Separate by type buys = [t for t in trades if t.get("type") == "TRADE" and t.get("side") == "BUY"] sells = [t for t in trades if t.get("type") == "TRADE" and t.get("side") == "SELL"] redeems = [t for t in trades if t.get("type") == "REDEEM"] print(f"BUYs: {len(buys)}, SELLs: {len(sells)}, REDEEMs: {len(redeems)}") # Group by market (conditionId) markets = defaultdict(lambda: {"buys": [], "sells": [], "redeems": [], "title": ""}) for t in trades: cid = t.get("conditionId", "") if not cid: continue markets[cid]["title"] = t.get("title", "") if t["type"] == "TRADE" and t.get("side") == "BUY": markets[cid]["buys"].append(t) elif t["type"] == "TRADE" and t.get("side") == "SELL": markets[cid]["sells"].append(t) elif t["type"] == "REDEEM": markets[cid]["redeems"].append(t) print(f"Unique markets: {len(markets)}") # Reconstruct P&L per market results = [] for cid, data in markets.items(): total_bought_usdc = sum(t.get("usdcSize", 0) for t in data["buys"]) total_bought_shares = sum(t.get("size", 0) for t in data["buys"]) total_sold_usdc = sum(t.get("usdcSize", 0) for t in data["sells"]) total_redeemed_usdc = sum(t.get("usdcSize", 0) for t in data["redeems"]) total_redeemed_shares = sum(t.get("size", 0) for t in data["redeems"]) # Net cost = bought - sold net_cost = total_bought_usdc - total_sold_usdc # Returns = redeemed amount returns = total_redeemed_usdc # If redeemed shares > 0 and usdc > 0, it was a win # If no redeems or redeem usdc=0, could be loss or still open pnl = returns - net_cost # Determine status if total_redeemed_shares > 0 and total_redeemed_usdc > 0: status = "WIN" elif total_redeemed_shares > 0 and total_redeemed_usdc == 0: status = "LOSS" # redeemed at 0 elif len(data["redeems"]) > 0: status = "LOSS" else: status = "OPEN" # Get timestamps all_times = [t.get("timestamp", 0) for t in data["buys"] + data["sells"] + data["redeems"]] first_trade = min(all_times) if all_times else 0 last_trade = max(all_times) if all_times else 0 avg_price = total_bought_usdc / total_bought_shares if total_bought_shares > 0 else 0 results.append({ "conditionId": cid, "title": data["title"], "status": status, "net_cost": round(net_cost, 2), "returns": round(returns, 2), "pnl": round(pnl, 2), "shares_bought": round(total_bought_shares, 2), "avg_price": round(avg_price, 4), "first_trade": first_trade, "last_trade": last_trade, "num_buys": len(data["buys"]), "num_sells": len(data["sells"]), "num_redeems": len(data["redeems"]), }) # Sort by first trade time results.sort(key=lambda x: x["first_trade"]) # Stats wins = [r for r in results if r["status"] == "WIN"] losses = [r for r in results if r["status"] == "LOSS"] opens = [r for r in results if r["status"] == "OPEN"] resolved = wins + losses total_cost = sum(r["net_cost"] for r in results) total_returns = sum(r["returns"] for r in results) total_pnl = sum(r["pnl"] for r in results) print(f"\n=== MARKET RESULTS ===") print(f"Wins: {len(wins)}, Losses: {len(losses)}, Open: {len(opens)}") print(f"Win rate (resolved): {len(wins)/len(resolved)*100:.1f}%" if resolved else "N/A") print(f"Total cost: ${total_cost:,.2f}") print(f"Total returns: ${total_returns:,.2f}") print(f"Total P&L: ${total_pnl:,.2f}") # Top wins and losses wins_sorted = sorted(wins, key=lambda x: x["pnl"], reverse=True) losses_sorted = sorted(losses, key=lambda x: x["pnl"]) print(f"\n=== TOP 10 WINS ===") for r in wins_sorted[:10]: dt = datetime.fromtimestamp(r["first_trade"]).strftime("%Y-%m-%d") if r["first_trade"] else "?" print(f" +${r['pnl']:>12,.2f} | {dt} | {r['title'][:60]}") print(f"\n=== TOP 10 LOSSES ===") for r in losses_sorted[:10]: dt = datetime.fromtimestamp(r["first_trade"]).strftime("%Y-%m-%d") if r["first_trade"] else "?" print(f" -${abs(r['pnl']):>12,.2f} | {dt} | {r['title'][:60]}") # === COPY TRADE SIMULATION === print(f"\n=== COPY-TRADE SIMULATION ($10,000 bankroll) ===") # Process all resolved markets chronologically resolved_chrono = sorted(resolved, key=lambda x: x["first_trade"]) for scenario_name, slippage in [("Instant", 0), ("30min delay", 0.05), ("1hr delay", 0.10)]: bankroll = 10000 peak = bankroll max_dd = 0 max_dd_pct = 0 streak = 0 max_losing_streak = 0 trade_results = [] for r in resolved_chrono: # Proportional sizing: his cost / his total capital * our bankroll # Use 1% of bankroll per bet as conservative sizing position_size = min(bankroll * 0.02, bankroll) # 2% per bet if position_size <= 0: continue # Adjust entry price for slippage entry_price = min(r["avg_price"] * (1 + slippage), 0.99) if r["status"] == "WIN": # Payout is $1 per share, cost was entry_price per share shares = position_size / entry_price payout = shares * 1.0 trade_pnl = payout - position_size streak = 0 else: trade_pnl = -position_size streak += 1 max_losing_streak = max(max_losing_streak, streak) bankroll += trade_pnl peak = max(peak, bankroll) dd = (peak - bankroll) / peak * 100 max_dd_pct = max(max_dd_pct, dd) trade_results.append(trade_pnl) total_trades = len(trade_results) wins_count = sum(1 for t in trade_results if t > 0) avg_win = sum(t for t in trade_results if t > 0) / wins_count if wins_count else 0 avg_loss = sum(t for t in trade_results if t <= 0) / (total_trades - wins_count) if (total_trades - wins_count) > 0 else 0 print(f"\n {scenario_name}:") print(f" Final bankroll: ${bankroll:,.2f} ({(bankroll/10000-1)*100:+.1f}%)") print(f" Trades: {total_trades}, Wins: {wins_count} ({wins_count/total_trades*100:.1f}%)") print(f" Avg win: ${avg_win:,.2f}, Avg loss: ${avg_loss:,.2f}") print(f" Max drawdown: {max_dd_pct:.1f}%") print(f" Max losing streak: {max_losing_streak}") # Also do proportional sizing (mirror his allocation %) print(f"\n=== PROPORTIONAL COPY (mirror his sizing) ===") his_total_capital = sum(r["net_cost"] for r in resolved_chrono if r["net_cost"] > 0) for scenario_name, slippage in [("Instant", 0), ("30min delay", 0.05), ("1hr delay", 0.10)]: bankroll = 10000 peak = bankroll max_dd_pct = 0 streak = 0 max_losing_streak = 0 for r in resolved_chrono: if r["net_cost"] <= 0: continue # Mirror his position weight weight = r["net_cost"] / his_total_capital position_size = bankroll * weight * 10 # scale up since weights are tiny with 400+ markets position_size = min(position_size, bankroll * 0.25) # cap at 25% of bankroll if position_size <= 0: continue entry_price = min(r["avg_price"] * (1 + slippage), 0.99) if r["status"] == "WIN": shares = position_size / entry_price payout = shares * 1.0 trade_pnl = payout - position_size streak = 0 else: trade_pnl = -position_size streak += 1 max_losing_streak = max(max_losing_streak, streak) bankroll += trade_pnl peak = max(peak, bankroll) dd = (peak - bankroll) / peak * 100 max_dd_pct = max(max_dd_pct, dd) print(f"\n {scenario_name}:") print(f" Final bankroll: ${bankroll:,.2f} ({(bankroll/10000-1)*100:+.1f}%)") print(f" Max drawdown: {max_dd_pct:.1f}%") print(f" Max losing streak: {max_losing_streak}") # Monthly breakdown print(f"\n=== MONTHLY P&L (his actual) ===") monthly = defaultdict(float) for r in results: if r["first_trade"]: month = datetime.fromtimestamp(r["first_trade"]).strftime("%Y-%m") monthly[month] += r["pnl"] for month in sorted(monthly.keys()): bar = "+" * int(monthly[month] / 50000) if monthly[month] > 0 else "-" * int(abs(monthly[month]) / 50000) print(f" {month}: ${monthly[month]:>12,.2f} {bar}")