#!/usr/bin/env python3 """ Paper trading simulator for Feed Hunter strategies. Tracks virtual positions, P&L, and performance metrics. No real money — everything is simulated. Usage: python3 simulator.py status # Show all active sims python3 simulator.py open # Open a paper position python3 simulator.py close # Close a position python3 simulator.py update # Update mark-to-market python3 simulator.py history # Show closed positions python3 simulator.py stats # Performance summary """ import argparse import json import os import sys import uuid from datetime import datetime, timezone from pathlib import Path DATA_DIR = Path(__file__).parent / "data" / "simulations" ACTIVE_FILE = DATA_DIR / "active.json" HISTORY_FILE = DATA_DIR / "history.json" CONFIG_FILE = Path(__file__).parent / "config.json" def load_config(): with open(CONFIG_FILE) as f: return json.load(f) def load_active(): if ACTIVE_FILE.exists(): with open(ACTIVE_FILE) as f: return json.load(f) return {"positions": [], "bankroll_used": 0} def save_active(data): DATA_DIR.mkdir(parents=True, exist_ok=True) with open(ACTIVE_FILE, "w") as f: json.dump(data, f, indent=2) def load_history(): if HISTORY_FILE.exists(): with open(HISTORY_FILE) as f: return json.load(f) return {"closed": []} def save_history(data): DATA_DIR.mkdir(parents=True, exist_ok=True) with open(HISTORY_FILE, "w") as f: json.dump(data, f, indent=2) def cmd_open(args): """Open a new paper position.""" config = load_config() sim_config = config["simulation"] active = load_active() details = json.loads(args.details) # Calculate position size bankroll = sim_config["default_bankroll"] max_pos = bankroll * sim_config["max_position_pct"] position_size = details.get("size", max_pos) position_size = min(position_size, max_pos) sim_id = str(uuid.uuid4())[:8] position = { "id": sim_id, "strategy": args.strategy, "opened_at": datetime.now(timezone.utc).isoformat(), "type": details.get("type", "long"), # long, short, bet "asset": details.get("asset", "unknown"), "entry_price": details.get("entry_price", 0), "size": position_size, "quantity": details.get("quantity", 0), "stop_loss": details.get("stop_loss"), "take_profit": details.get("take_profit"), "current_price": details.get("entry_price", 0), "unrealized_pnl": 0, "unrealized_pnl_pct": 0, "source_post": details.get("source_post", ""), "thesis": details.get("thesis", ""), "notes": details.get("notes", ""), "updates": [], } active["positions"].append(position) active["bankroll_used"] = sum(p["size"] for p in active["positions"]) save_active(active) print(f"✅ Paper position opened: {sim_id}") print(f" Strategy: {args.strategy}") print(f" Asset: {position['asset']}") print(f" Type: {position['type']}") print(f" Entry: ${position['entry_price']}") print(f" Size: ${position_size:.2f}") if position["stop_loss"]: print(f" Stop Loss: ${position['stop_loss']}") if position["take_profit"]: print(f" Take Profit: ${position['take_profit']}") def cmd_close(args): """Close a paper position.""" active = load_active() history = load_history() pos = None for i, p in enumerate(active["positions"]): if p["id"] == args.sim_id: pos = active["positions"].pop(i) break if not pos: print(f"❌ Position {args.sim_id} not found") sys.exit(1) exit_price = float(args.exit_price) entry_price = pos["entry_price"] if pos["type"] == "long": pnl_pct = (exit_price - entry_price) / entry_price if entry_price else 0 elif pos["type"] == "short": pnl_pct = (entry_price - exit_price) / entry_price if entry_price else 0 elif pos["type"] == "bet": # For binary bets: exit_price is 1 (win) or 0 (lose) pnl_pct = (exit_price - entry_price) / entry_price if entry_price else 0 else: pnl_pct = 0 realized_pnl = pos["size"] * pnl_pct pos["closed_at"] = datetime.now(timezone.utc).isoformat() pos["exit_price"] = exit_price pos["realized_pnl"] = round(realized_pnl, 2) pos["realized_pnl_pct"] = round(pnl_pct * 100, 2) history["closed"].append(pos) active["bankroll_used"] = sum(p["size"] for p in active["positions"]) save_active(active) save_history(history) emoji = "🟢" if realized_pnl >= 0 else "🔴" print(f"{emoji} Position closed: {pos['id']}") print(f" Asset: {pos['asset']}") print(f" Entry: ${entry_price} → Exit: ${exit_price}") print(f" P&L: ${realized_pnl:+.2f} ({pnl_pct*100:+.1f}%)") def cmd_update(args): """Update mark-to-market for a position.""" active = load_active() for pos in active["positions"]: if pos["id"] == args.sim_id: current = float(args.current_price) entry = pos["entry_price"] if pos["type"] == "long": pnl_pct = (current - entry) / entry if entry else 0 elif pos["type"] == "short": pnl_pct = (entry - current) / entry if entry else 0 else: pnl_pct = (current - entry) / entry if entry else 0 pos["current_price"] = current pos["unrealized_pnl"] = round(pos["size"] * pnl_pct, 2) pos["unrealized_pnl_pct"] = round(pnl_pct * 100, 2) pos["updates"].append({ "time": datetime.now(timezone.utc).isoformat(), "price": current, "pnl": pos["unrealized_pnl"], }) # Check stop loss if pos.get("stop_loss") and pos["type"] == "long" and current <= pos["stop_loss"]: print(f"⚠️ STOP LOSS triggered for {pos['id']} at ${current}") if pos.get("take_profit") and pos["type"] == "long" and current >= pos["take_profit"]: print(f"🎯 TAKE PROFIT hit for {pos['id']} at ${current}") save_active(active) emoji = "🟢" if pos["unrealized_pnl"] >= 0 else "🔴" print(f"{emoji} Updated {pos['id']}: ${current} ({pos['unrealized_pnl_pct']:+.1f}%)") return print(f"❌ Position {args.sim_id} not found") def cmd_status(args): """Show all active positions.""" active = load_active() config = load_config() bankroll = config["simulation"]["default_bankroll"] if not active["positions"]: print("No active paper positions.") return total_unrealized = 0 print(f"=== Active Paper Positions ===") print(f"Bankroll: ${bankroll} | Used: ${active['bankroll_used']:.2f} | Free: ${bankroll - active['bankroll_used']:.2f}\n") for pos in active["positions"]: emoji = "🟢" if pos["unrealized_pnl"] >= 0 else "🔴" total_unrealized += pos["unrealized_pnl"] print(f"{emoji} [{pos['id']}] {pos['strategy']}") print(f" {pos['asset']} | {pos['type']} | Size: ${pos['size']:.2f}") print(f" Entry: ${pos['entry_price']} → Current: ${pos['current_price']}") print(f" P&L: ${pos['unrealized_pnl']:+.2f} ({pos['unrealized_pnl_pct']:+.1f}%)") print(f" Opened: {pos['opened_at'][:16]}") if pos.get("thesis"): print(f" Thesis: {pos['thesis'][:80]}") print() print(f"Total unrealized P&L: ${total_unrealized:+.2f}") def cmd_history(args): """Show closed positions.""" history = load_history() if not history["closed"]: print("No closed positions yet.") return print("=== Closed Positions ===\n") for pos in history["closed"]: emoji = "🟢" if pos["realized_pnl"] >= 0 else "🔴" print(f"{emoji} [{pos['id']}] {pos['strategy']}") print(f" {pos['asset']} | ${pos['entry_price']} → ${pos['exit_price']}") print(f" P&L: ${pos['realized_pnl']:+.2f} ({pos['realized_pnl_pct']:+.1f}%)") print(f" {pos['opened_at'][:16]} → {pos['closed_at'][:16]}") print() def cmd_stats(args): """Performance summary across all closed trades.""" history = load_history() config = load_config() closed = history.get("closed", []) if not closed: print("No completed trades to analyze.") return wins = [t for t in closed if t["realized_pnl"] > 0] losses = [t for t in closed if t["realized_pnl"] <= 0] total_pnl = sum(t["realized_pnl"] for t in closed) print("=== Performance Summary ===\n") print(f"Total trades: {len(closed)}") print(f"Wins: {len(wins)} | Losses: {len(losses)}") print(f"Win rate: {len(wins)/len(closed)*100:.1f}%") print(f"Total P&L: ${total_pnl:+.2f}") if wins: avg_win = sum(t["realized_pnl"] for t in wins) / len(wins) best = max(closed, key=lambda t: t["realized_pnl"]) print(f"Avg win: ${avg_win:+.2f}") print(f"Best trade: {best['id']} ({best['strategy']}) ${best['realized_pnl']:+.2f}") if losses: avg_loss = sum(t["realized_pnl"] for t in losses) / len(losses) worst = min(closed, key=lambda t: t["realized_pnl"]) print(f"Avg loss: ${avg_loss:+.2f}") print(f"Worst trade: {worst['id']} ({worst['strategy']}) ${worst['realized_pnl']:+.2f}") # ROI bankroll = config["simulation"]["default_bankroll"] roi = (total_pnl / bankroll) * 100 print(f"\nROI on ${bankroll} bankroll: {roi:+.1f}%") # By strategy strategies = {} for t in closed: s = t["strategy"] if s not in strategies: strategies[s] = {"trades": 0, "pnl": 0, "wins": 0} strategies[s]["trades"] += 1 strategies[s]["pnl"] += t["realized_pnl"] if t["realized_pnl"] > 0: strategies[s]["wins"] += 1 if len(strategies) > 1: print(f"\n=== By Strategy ===") for name, data in sorted(strategies.items(), key=lambda x: x[1]["pnl"], reverse=True): wr = data["wins"] / data["trades"] * 100 print(f" {name}: {data['trades']} trades, {wr:.0f}% WR, ${data['pnl']:+.2f}") def main(): parser = argparse.ArgumentParser(description="Feed Hunter Paper Trading Simulator") sub = parser.add_subparsers(dest="command") sub.add_parser("status", help="Show active positions") p_open = sub.add_parser("open", help="Open paper position") p_open.add_argument("strategy", help="Strategy name") p_open.add_argument("details", help="JSON with position details") p_close = sub.add_parser("close", help="Close position") p_close.add_argument("sim_id", help="Position ID") p_close.add_argument("exit_price", help="Exit price") p_update = sub.add_parser("update", help="Update mark-to-market") p_update.add_argument("sim_id", help="Position ID") p_update.add_argument("current_price", help="Current price") sub.add_parser("history", help="Closed positions") sub.add_parser("stats", help="Performance summary") args = parser.parse_args() if args.command == "status": cmd_status(args) elif args.command == "open": cmd_open(args) elif args.command == "close": cmd_close(args) elif args.command == "update": cmd_update(args) elif args.command == "history": cmd_history(args) elif args.command == "stats": cmd_stats(args) else: parser.print_help() if __name__ == "__main__": main()