#!/usr/bin/env python3 # ============================================================================== # COPYRIGHT NO ONE EVERYWHERE LLC (WYOMING HOLDING COMPANY) # PROJECT: SOVEREIGN STACK # This artifact is entirely proprietary and cryptographically proven. # Open-Source usage requires explicit permission from Brandon Scott Schneider. # ============================================================================== """ Paper Trading Simulator utilities. Modes: - fast-sweep: synthetic compressed-time Monte Carlo sweep for strategy exploration - wall-clock: real-duration paper trading session with heartbeat logging - finalize-summary: turn a completed wall-clock session into a one-page summary """ import argparse import csv import hashlib import json import math import random import re import time from dataclasses import asdict, dataclass from datetime import datetime, timedelta, timezone from pathlib import Path from statistics import mean, median from typing import Any, Dict, List, Optional, Tuple def utc_now() -> datetime: """Return a timezone-aware UTC timestamp.""" return datetime.now(timezone.utc) def format_utc(dt: datetime) -> str: """Render timestamps with a trailing Z for consistency across artifacts.""" return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") def clamp_near_zero(value: float, epsilon: float = 1e-12) -> float: """Avoid tiny float residue in positions and cost basis values.""" return 0.0 if abs(value) < epsilon else value def write_json(path: Path, payload: Dict[str, Any]) -> None: """Write a JSON payload to disk, creating parent directories if needed.""" path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as handle: json.dump(payload, handle, indent=2) def append_text(path: Path, text: str) -> None: """Append text to a file, creating parent directories if needed.""" path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: handle.write(text) @dataclass class Trade: """Individual trade record.""" timestamp: str type: str symbol: str amount_usdc: float price_usd: float quantity: float position_after: float cost_basis_usdc: Optional[float] pnl: Optional[float] tx_id: str class PaperTradingSimulator: """Simulates trading with paper money and tracks realized cost basis.""" def __init__( self, initial_usdc: float = 100.0, duration_hours: float = 1.0, session_mode: str = "wall_clock_session", random_seed: Optional[int] = None, ): self.initial_usdc = initial_usdc self.current_usdc = initial_usdc self.duration_hours = duration_hours self.session_mode = session_mode self.random = random.Random(random_seed) self.start_time = utc_now() self.end_time = self.start_time + timedelta(hours=duration_hours) self.positions: Dict[str, float] = {} self.position_cost_basis_usdc: Dict[str, float] = {} self.price_history: Dict[str, List[Tuple[str, float]]] = { "ETH": [], "BTC": [], "SOL": [], "ARB": [], } self.trades: List[Trade] = [] self.current_prices: Dict[str, float] = { "ETH": 2100.00 + self.random.uniform(-50, 50), "BTC": 42000.00 + self.random.uniform(-1000, 1000), "SOL": 98.00 + self.random.uniform(-5, 5), "ARB": 2.50 + self.random.uniform(-0.1, 0.1), } self.decision_log: List[Dict[str, Any]] = [] self.portfolio_history: List[Dict[str, Any]] = [] def generate_market_data(self, timestamp: Optional[datetime] = None) -> Dict[str, float]: """Generate the next simulated market prices.""" tick_time = timestamp or utc_now() new_prices = {} volatility = { "ETH": 0.015, "BTC": 0.012, "SOL": 0.025, "ARB": 0.030, } for symbol, current_price in self.current_prices.items(): drift = self.random.uniform(-volatility[symbol], volatility[symbol]) new_price = max(current_price * (1 + drift), 0.01) new_prices[symbol] = new_price self.price_history[symbol].append((format_utc(tick_time), new_price)) self.current_prices = new_prices return new_prices def should_trade(self, symbol: str, price: float) -> Tuple[str, float]: """Return an action and unit-consistent amount for the next trade.""" recent_prices = [point[1] for point in self.price_history[symbol][-5:]] if len(recent_prices) < 2: return ("hold", 0.0) avg_recent = sum(recent_prices) / len(recent_prices) if price < avg_recent * 0.98 and self.current_usdc > 10: buy_amount_usdc = min(self.current_usdc * 0.25, self.current_usdc) return ("buy", buy_amount_usdc) if price > avg_recent * 1.02 and self.positions.get(symbol, 0.0) > 0: sell_quantity = self.positions[symbol] * 0.5 return ("sell", sell_quantity) return ("hold", 0.0) def average_cost_per_unit(self, symbol: str) -> float: """Return average cost per unit for the current position.""" quantity = self.positions.get(symbol, 0.0) if quantity <= 0: return 0.0 return self.position_cost_basis_usdc.get(symbol, 0.0) / quantity def execute_trade( self, symbol: str, action: str, amount: float, timestamp: Optional[datetime] = None, ) -> Optional[Trade]: """Execute a trade. Buy amounts are denominated in USDC. Sell amounts are denominated in asset quantity. """ if amount <= 0: return None price = self.current_prices[symbol] trade_time = timestamp or utc_now() trade_timestamp = format_utc(trade_time) if action == "buy": if amount > self.current_usdc: return None quantity = amount / price self.current_usdc -= amount self.positions[symbol] = self.positions.get(symbol, 0.0) + quantity self.position_cost_basis_usdc[symbol] = self.position_cost_basis_usdc.get(symbol, 0.0) + amount trade = Trade( timestamp=trade_timestamp, type="buy", symbol=symbol, amount_usdc=amount, price_usd=price, quantity=quantity, position_after=self.positions[symbol], cost_basis_usdc=amount, pnl=None, tx_id=hashlib.sha256(f"{symbol}|buy|{trade_timestamp}".encode()).hexdigest()[:16], ) self.trades.append(trade) return trade if action == "sell": current_quantity = self.positions.get(symbol, 0.0) if amount > current_quantity: return None avg_cost = self.average_cost_per_unit(symbol) cost_basis_sold = avg_cost * amount proceeds = amount * price self.current_usdc += proceeds remaining_quantity = clamp_near_zero(current_quantity - amount) remaining_cost_basis = clamp_near_zero( self.position_cost_basis_usdc.get(symbol, 0.0) - cost_basis_sold ) self.positions[symbol] = remaining_quantity self.position_cost_basis_usdc[symbol] = remaining_cost_basis if remaining_quantity == 0.0: self.positions.pop(symbol, None) self.position_cost_basis_usdc.pop(symbol, None) pnl = proceeds - cost_basis_sold trade = Trade( timestamp=trade_timestamp, type="sell", symbol=symbol, amount_usdc=proceeds, price_usd=price, quantity=amount, position_after=remaining_quantity, cost_basis_usdc=cost_basis_sold, pnl=pnl, tx_id=hashlib.sha256(f"{symbol}|sell|{trade_timestamp}".encode()).hexdigest()[:16], ) self.trades.append(trade) return trade return None def calculate_portfolio_value(self) -> float: """Calculate total portfolio value at current prices.""" value = self.current_usdc for symbol, quantity in self.positions.items(): value += quantity * self.current_prices[symbol] return value def record_portfolio_snapshot(self, timestamp: Optional[datetime] = None, tick: Optional[int] = None) -> None: """Store a mark-to-market snapshot for reporting and finalization.""" snapshot_time = timestamp or utc_now() self.portfolio_history.append( { "timestamp": format_utc(snapshot_time), "tick": tick, "portfolio_value_usdc": self.calculate_portfolio_value(), "cash_usdc": self.current_usdc, "trade_count": len(self.trades), } ) def step(self, tick: int, timestamp: Optional[datetime] = None) -> None: """Advance the simulation by one tick.""" step_time = timestamp or utc_now() prices = self.generate_market_data(step_time) for symbol in prices.keys(): action, amount = self.should_trade(symbol, prices[symbol]) if action == "hold": continue trade = self.execute_trade(symbol, action, amount, step_time) self.decision_log.append( { "tick": tick, "timestamp": format_utc(step_time), "symbol": symbol, "action": action, "signal_amount": amount, "price": prices[symbol], "executed": trade is not None, } ) self.record_portfolio_snapshot(step_time, tick) def generate_report( self, session_end_time: Optional[datetime] = None, tick_count: int = 0, tick_interval_seconds: float = 0.0, ) -> Dict[str, Any]: """Generate a complete session report.""" end_time = session_end_time or utc_now() portfolio_value = self.calculate_portfolio_value() total_pnl = portfolio_value - self.initial_usdc realized_pnl = sum(trade.pnl for trade in self.trades if trade.pnl is not None) unrealized_pnl = total_pnl - realized_pnl pnl_percent = (total_pnl / self.initial_usdc * 100) if self.initial_usdc > 0 else 0.0 max_portfolio_value = max( [snapshot["portfolio_value_usdc"] for snapshot in self.portfolio_history], default=portfolio_value, ) return { "report_type": self.session_mode, "comparability_class": ( "synthetic_compressed_time" if self.session_mode.startswith("fast_") else "wall_clock" ), "comparability_note": ( "Synthetic compressed-time output is not directly comparable to wall-clock session results." if self.session_mode.startswith("fast_") else "Wall-clock session output reflects real elapsed time and should not be compared to synthetic sweeps as if they were the same instrument." ), "simulation_start_utc": format_utc(self.start_time), "simulation_end_utc": format_utc(end_time), "duration_hours_requested": self.duration_hours, "tick_interval_seconds": tick_interval_seconds, "tick_count": tick_count, "initial_capital_usdc": self.initial_usdc, "current_usdc": self.current_usdc, "portfolio_value_usdc": portfolio_value, "max_portfolio_value_usdc": max_portfolio_value, "total_pnl_usdc": total_pnl, "realized_pnl_usdc": realized_pnl, "unrealized_pnl_usdc": unrealized_pnl, "pnl_percent": pnl_percent, "trade_count": len(self.trades), "final_positions": { symbol: { "quantity": qty, "price": self.current_prices[symbol], "value": qty * self.current_prices[symbol], "cost_basis_usdc": self.position_cost_basis_usdc.get(symbol, 0.0), "average_cost_usdc": self.average_cost_per_unit(symbol), } for symbol, qty in self.positions.items() if qty > 0 }, "final_prices": self.current_prices, "trades": [asdict(trade) for trade in self.trades[-20:]], "decision_log_sample": self.decision_log[-10:], "portfolio_history_sample": self.portfolio_history[-10:], } def run_compressed_time(self, tick_interval_seconds: float = 10.0) -> Dict[str, Any]: """Run a synthetic compressed-time path with no sleeping.""" total_seconds = self.duration_hours * 3600 tick_count = max(1, math.ceil(total_seconds / tick_interval_seconds)) for tick in range(1, tick_count + 1): tick_time = self.start_time + timedelta(seconds=tick * tick_interval_seconds) self.step(tick, tick_time) simulated_end = self.start_time + timedelta(seconds=tick_count * tick_interval_seconds) return self.generate_report( session_end_time=simulated_end, tick_count=tick_count, tick_interval_seconds=tick_interval_seconds, ) def run_wall_clock( self, tick_interval_seconds: float = 60.0, heartbeat_path: Optional[Path] = None, heartbeat_every_ticks: int = 5, ) -> Dict[str, Any]: """Run a real-duration wall-clock session with optional heartbeat logging.""" total_seconds = self.duration_hours * 3600 tick_count = max(1, math.ceil(total_seconds / tick_interval_seconds)) if heartbeat_path: heartbeat_path.parent.mkdir(parents=True, exist_ok=True) heartbeat_path.write_text(f"start_utc={format_utc(self.start_time)}\n", encoding="utf-8") for tick in range(1, tick_count + 1): tick_time = utc_now() self.step(tick, tick_time) if heartbeat_path and (tick % heartbeat_every_ticks == 0 or tick == tick_count): append_text( heartbeat_path, ( f"{format_utc(tick_time)} tick={tick} " f"portfolio_value_usdc={self.calculate_portfolio_value():.2f} " f"cash_usdc={self.current_usdc:.2f} trades={len(self.trades)}\n" ), ) if tick < tick_count: time.sleep(tick_interval_seconds) end_time = utc_now() if heartbeat_path: append_text(heartbeat_path, f"end_utc={format_utc(end_time)}\n") return self.generate_report( session_end_time=end_time, tick_count=tick_count, tick_interval_seconds=tick_interval_seconds, ) HEARTBEAT_LINE = re.compile( r"^(?P\S+) tick=(?P\d+) portfolio_value_usdc=(?P[\d.\-]+) cash_usdc=(?P[\d.\-]+) trades=(?P\d+)$" ) def parse_heartbeat_log(heartbeat_path: Path) -> Dict[str, Any]: """Parse wall-clock heartbeat log entries.""" start_utc = None end_utc = None entries: List[Dict[str, Any]] = [] for raw_line in heartbeat_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line: continue if line.startswith("start_utc="): start_utc = line.split("=", 1)[1] continue if line.startswith("end_utc="): end_utc = line.split("=", 1)[1] continue match = HEARTBEAT_LINE.match(line) if not match: continue entries.append( { "timestamp": match.group("timestamp"), "tick": int(match.group("tick")), "portfolio_value_usdc": float(match.group("portfolio")), "cash_usdc": float(match.group("cash")), "trade_count": int(match.group("trades")), } ) return { "start_utc": start_utc, "end_utc": end_utc, "entries": entries, } def format_money(value: float) -> str: """Format a numeric value for the summary packet.""" return f"{value:,.2f}" def relative_or_absolute(path: Optional[Path], base: Path) -> str: """Prefer relative paths inside a cycle packet for readability.""" if path is None: return "pending" try: return str(path.relative_to(base)) except ValueError: return str(path) def derive_cycle_metrics( report: Dict[str, Any], objective_target: float, gross_exit_ceiling: float, ) -> Dict[str, Any]: """Compute summary and ledger metrics from a completed session report.""" final_value = report["portfolio_value_usdc"] fluctuation_band = max(gross_exit_ceiling - objective_target, 0.0) gain_above_baseline = final_value - objective_target progress_toward_objective = min(max(gain_above_baseline, 0.0), objective_target) progress_within_buffer = min(max(gain_above_baseline, 0.0), fluctuation_band) bonus_upside = max(final_value - gross_exit_ceiling, 0.0) return { "final_value": final_value, "gain_above_baseline": gain_above_baseline, "fluctuation_band": fluctuation_band, "progress_toward_objective": progress_toward_objective, "progress_within_buffer": progress_within_buffer, "bonus_upside": bonus_upside, "over_ceiling_flag": "YES" if final_value > gross_exit_ceiling else "NO", } def sync_reconciliation_ledger( cycle_dir: Path, session_report_path: Path, heartbeat_log_path: Path, review_log_path: Optional[Path] = None, objective_target: float = 30000.0, gross_exit_ceiling: float = 37000.0, ledger_output_path: Optional[Path] = None, ) -> Path: """Update the cycle reconciliation ledger from the completed session report.""" report = json.loads(session_report_path.read_text(encoding="utf-8")) heartbeat = parse_heartbeat_log(heartbeat_log_path) metrics = derive_cycle_metrics(report, objective_target, gross_exit_ceiling) ledger_path = ledger_output_path or (cycle_dir / "07_allocation_ledger" / "reconciliation_ledger.csv") fieldnames = [ "cycle_id", "record_utc", "record_type", "provider", "reference_id", "asset", "amount", "fee", "from_account", "to_account", "purpose", "objective_progress_usd", "buffer_usage_usd", "over_ceiling_flag", "bonus_upside_usd", "notes", ] preserved_rows: List[Dict[str, str]] = [] managed_record_types = { "wall_clock_session_start", "wall_clock_session_final", "review_log_reference", } if ledger_path.exists(): with ledger_path.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) for row in reader: if row.get("record_type") not in managed_record_types: preserved_rows.append(row) start_utc = heartbeat["start_utc"] or report["simulation_start_utc"] end_utc = heartbeat["end_utc"] or report["simulation_end_utc"] session_reference = session_report_path.stem heartbeat_reference = heartbeat_log_path.stem managed_rows = [ { "cycle_id": cycle_dir.name, "record_utc": start_utc, "record_type": "wall_clock_session_start", "provider": "local_sim", "reference_id": heartbeat_reference, "asset": "USD", "amount": f"{report['initial_capital_usdc']:.2f}", "fee": "0.00", "from_account": "paper_capital", "to_account": "realtime_paper_portfolio", "purpose": "wall_clock_session_start", "objective_progress_usd": "0.00", "buffer_usage_usd": "0.00", "over_ceiling_flag": "NO", "bonus_upside_usd": "0.00", "notes": "Auto-synced wall-clock session start", }, { "cycle_id": cycle_dir.name, "record_utc": end_utc, "record_type": "wall_clock_session_final", "provider": "local_sim", "reference_id": session_reference, "asset": "USD", "amount": f"{metrics['final_value']:.2f}", "fee": "0.00", "from_account": "realtime_paper_portfolio", "to_account": "paper_portfolio_close", "purpose": "wall_clock_session_final", "objective_progress_usd": f"{metrics['progress_toward_objective']:.2f}", "buffer_usage_usd": f"{metrics['progress_within_buffer']:.2f}", "over_ceiling_flag": metrics["over_ceiling_flag"], "bonus_upside_usd": f"{metrics['bonus_upside']:.2f}", "notes": ( f"Auto-synced from {relative_or_absolute(session_report_path, cycle_dir)}; " f"realized_pnl_usdc={report['realized_pnl_usdc']:.2f}; trade_count={report['trade_count']}" ), }, ] if review_log_path: managed_rows.append( { "cycle_id": cycle_dir.name, "record_utc": start_utc, "record_type": "review_log_reference", "provider": "local_review", "reference_id": review_log_path.stem, "asset": "USD", "amount": "0.00", "fee": "0.00", "from_account": "realtime_heartbeat", "to_account": "review_log", "purpose": "review_reference", "objective_progress_usd": f"{metrics['progress_toward_objective']:.2f}", "buffer_usage_usd": f"{metrics['progress_within_buffer']:.2f}", "over_ceiling_flag": metrics["over_ceiling_flag"], "bonus_upside_usd": f"{metrics['bonus_upside']:.2f}", "notes": f"Review log reference: {relative_or_absolute(review_log_path, cycle_dir)}", } ) combined_rows = preserved_rows + managed_rows ledger_path.parent.mkdir(parents=True, exist_ok=True) with ledger_path.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(combined_rows) return ledger_path def build_one_page_summary( cycle_dir: Path, session_report_path: Path, heartbeat_log_path: Path, review_log_path: Optional[Path] = None, reviewer: str = "system-generated", objective_target: float = 30000.0, gross_exit_ceiling: float = 37000.0, ) -> str: """Build the final one-page markdown summary for a completed wall-clock session.""" report = json.loads(session_report_path.read_text(encoding="utf-8")) heartbeat = parse_heartbeat_log(heartbeat_log_path) metrics = derive_cycle_metrics(report, objective_target, gross_exit_ceiling) final_value = metrics["final_value"] max_value = max( [entry["portfolio_value_usdc"] for entry in heartbeat["entries"]] + [report["max_portfolio_value_usdc"]] ) realized_gain_loss = report["realized_pnl_usdc"] start_utc = heartbeat["start_utc"] or report["simulation_start_utc"] end_utc = heartbeat["end_utc"] or report["simulation_end_utc"] lines = [ "# One-Page Money Trail Summary", "", "## Cycle Header", f"- Cycle ID: {cycle_dir.name}", f"- Window UTC: {start_utc} to {end_utc}", f"- Prepared UTC: {format_utc(utc_now())}", f"- Reviewer: {reviewer}", "", "## Portfolio Snapshot", f"- Starting value (USD): {format_money(report['initial_capital_usdc'])}", f"- Objective target value (USD): {format_money(objective_target)}", f"- Gross exit ceiling (USD): {format_money(gross_exit_ceiling)}", f"- Bonus upside above ceiling (USD): {format_money(metrics['bonus_upside'])}", f"- Final value (USD): {format_money(final_value)}", f"- Max value seen (USD): {format_money(max_value)}", f"- Realized gain/loss (USD): {format_money(realized_gain_loss)}", f"- Trade count: {report['trade_count']}", f"- Session report type: {report['report_type']}", "", "## Transfer Chain", f"- Strategy source artifact: {relative_or_absolute(session_report_path, cycle_dir)}", f"- Realtime heartbeat artifact: {relative_or_absolute(heartbeat_log_path, cycle_dir)}", f"- Review heartbeat artifact: {relative_or_absolute(review_log_path, cycle_dir)}" if review_log_path else "- Review heartbeat artifact: pending / not provided", "- Wallet transfer artifact(s): pending (paper cycle only)", "- Coinbase export artifact: pending (no live off-ramp activity)", "- Bank confirmation artifact: pending (no live off-ramp activity)", "", "## Reconciliation Results", "- Asset conservation: PASS for paper-cycle artifacts only", "- Fiat settlement match: NOT APPLICABLE", "- Time-order integrity: PASS for wall-clock heartbeat and final report", "- ID completeness: PARTIAL (paper logs only; no exchange/bank IDs yet)", "", "## Tax and Allocation", "- Tax reserve moved (USD): 0.00", "- Debt bucket moved (USD): 0.00", "- Vehicle bucket moved (USD): 0.00", f"- Retained operating cash (USD): {format_money(final_value)} paper value", "", f"## Goal Progress ({format_money(objective_target)} objective / {format_money(gross_exit_ceiling)} ceiling)", f"- Gain above {format_money(objective_target)} baseline (USD): {format_money(metrics['gain_above_baseline'])}", f"- Progress toward {format_money(objective_target)} objective (USD): {format_money(metrics['progress_toward_objective'])} / {format_money(objective_target)}", f"- Progress within {format_money(metrics['fluctuation_band'])} fluctuation buffer (USD): {format_money(metrics['progress_within_buffer'])} / {format_money(metrics['fluctuation_band'])}", f"- Over-ceiling flag: {metrics['over_ceiling_flag']}", f"- Bonus upside beyond {format_money(gross_exit_ceiling)} (USD): {format_money(metrics['bonus_upside'])}", "- Debt target progress (15,000): 0.00 / 15,000.00", "- Vehicle target progress (15,000): 0.00 / 15,000.00", f"- Combined target progress ({format_money(objective_target)} objective): {format_money(metrics['progress_toward_objective'])} / {format_money(objective_target)}", "", "## Exceptions and Notes", "- This packet documents a completed wall-clock paper session rather than a synthetic sweep.", "- Realized PnL now reflects tracked position cost basis; unrealized exposure remains inside the marked portfolio value.", "- This packet currently documents paper strategy evidence only, not live execution, Coinbase settlement, or bank receipt.", "- If realized value ends above the gross ceiling, the excess is treated as bonus upside rather than required plan performance.", "", "## Process Integrity Statement", "- This report is intended to show disciplined accounting through a volatile period, including losses if they occur.", "- The purpose is documentation and reconciliation, not trying to present a synthetic sweep as a real session.", "", "## Evidence Links", f"- {relative_or_absolute(session_report_path, cycle_dir)}", f"- {relative_or_absolute(heartbeat_log_path, cycle_dir)}", ] if review_log_path: lines.append(f"- {relative_or_absolute(review_log_path, cycle_dir)}") return "\n".join(lines) + "\n" def finalize_one_page_summary( cycle_dir: Path, session_report_path: Path, heartbeat_log_path: Path, review_log_path: Optional[Path] = None, summary_output_path: Optional[Path] = None, reviewer: str = "system-generated", objective_target: float = 30000.0, gross_exit_ceiling: float = 37000.0, ) -> Path: """Write the completed one-page summary for a finished wall-clock run.""" summary_path = summary_output_path or ( cycle_dir / "08_summary_report" / f"one_page_summary_filled_{utc_now().date().isoformat()}.md" ) summary_markdown = build_one_page_summary( cycle_dir=cycle_dir, session_report_path=session_report_path, heartbeat_log_path=heartbeat_log_path, review_log_path=review_log_path, reviewer=reviewer, objective_target=objective_target, gross_exit_ceiling=gross_exit_ceiling, ) summary_path.parent.mkdir(parents=True, exist_ok=True) summary_path.write_text(summary_markdown, encoding="utf-8") sync_reconciliation_ledger( cycle_dir=cycle_dir, session_report_path=session_report_path, heartbeat_log_path=heartbeat_log_path, review_log_path=review_log_path, objective_target=objective_target, gross_exit_ceiling=gross_exit_ceiling, ) return summary_path def run_fast_monte_carlo_sweep( initial_usdc: float, duration_hours: float, tick_interval_seconds: float, sweep_count: int, output_path: Optional[Path], seed: Optional[int], ) -> Dict[str, Any]: """Run a compressed-time Monte Carlo sweep and write aggregate results.""" run_summaries = [] for index in range(sweep_count): run_seed = None if seed is None else seed + index simulator = PaperTradingSimulator( initial_usdc=initial_usdc, duration_hours=duration_hours, session_mode="fast_monte_carlo_path", random_seed=run_seed, ) report = simulator.run_compressed_time(tick_interval_seconds=tick_interval_seconds) run_summaries.append( { "run_index": index, "portfolio_value_usdc": report["portfolio_value_usdc"], "total_pnl_usdc": report["total_pnl_usdc"], "realized_pnl_usdc": report["realized_pnl_usdc"], "unrealized_pnl_usdc": report["unrealized_pnl_usdc"], "pnl_percent": report["pnl_percent"], "trade_count": report["trade_count"], "max_portfolio_value_usdc": report["max_portfolio_value_usdc"], } ) portfolio_values = [run["portfolio_value_usdc"] for run in run_summaries] aggregate_report = { "report_type": "fast_monte_carlo_sweep", "comparability_class": "synthetic_compressed_time", "comparability_note": "This sweep explores many compressed-time synthetic paths. It is not directly comparable to a single wall-clock paper session.", "duration_hours_requested": duration_hours, "initial_capital_usdc": initial_usdc, "tick_interval_seconds": tick_interval_seconds, "sweep_count": sweep_count, "mean_portfolio_value_usdc": mean(portfolio_values), "median_portfolio_value_usdc": median(portfolio_values), "best_portfolio_value_usdc": max(portfolio_values), "worst_portfolio_value_usdc": min(portfolio_values), "positive_run_ratio": sum(1 for run in run_summaries if run["total_pnl_usdc"] > 0) / sweep_count, "run_summaries": run_summaries, } if output_path: write_json(output_path, aggregate_report) print("Starting fast Monte Carlo sweep...") print(f" Initial Capital: ${initial_usdc:.2f} USDC") print(f" Duration per Path: {duration_hours} hour(s)") print(f" Sweep Count: {sweep_count}") print(f" Mean Final Value: ${aggregate_report['mean_portfolio_value_usdc']:.2f}") print(f" Median Final Value: ${aggregate_report['median_portfolio_value_usdc']:.2f}") print(f" Best / Worst: ${aggregate_report['best_portfolio_value_usdc']:.2f} / ${aggregate_report['worst_portfolio_value_usdc']:.2f}") if output_path: print(f"\nāœ“ Sweep report written to {output_path}") return aggregate_report def run_wall_clock_session( initial_usdc: float, duration_hours: float, tick_interval_seconds: float, heartbeat_every_ticks: int, output_path: Optional[Path], heartbeat_path: Optional[Path], cycle_dir: Optional[Path], review_log_path: Optional[Path], summary_output_path: Optional[Path], reviewer: str, objective_target: float, gross_exit_ceiling: float, seed: Optional[int], ) -> Dict[str, Any]: """Run a real-duration paper session and optionally auto-finalize the cycle summary.""" print("Starting wall-clock paper trading session...") print(f" Initial Capital: ${initial_usdc:.2f} USDC") print(f" Duration: {duration_hours} hour(s)") print(f" Tick Interval: {tick_interval_seconds:.2f} second(s)") print(f" Start Time: {format_utc(utc_now())}\n") simulator = PaperTradingSimulator( initial_usdc=initial_usdc, duration_hours=duration_hours, session_mode="wall_clock_session", random_seed=seed, ) report = simulator.run_wall_clock( tick_interval_seconds=tick_interval_seconds, heartbeat_path=heartbeat_path, heartbeat_every_ticks=heartbeat_every_ticks, ) if output_path: write_json(output_path, report) print(f"āœ“ Session report written to {output_path}") if cycle_dir and output_path and heartbeat_path: summary_path = finalize_one_page_summary( cycle_dir=cycle_dir, session_report_path=output_path, heartbeat_log_path=heartbeat_path, review_log_path=review_log_path, summary_output_path=summary_output_path, reviewer=reviewer, objective_target=objective_target, gross_exit_ceiling=gross_exit_ceiling, ) print(f"āœ“ One-page summary written to {summary_path}") print("\nWall-clock session complete") print(f" Final Value: ${report['portfolio_value_usdc']:.2f}") print(f" Realized PnL: ${report['realized_pnl_usdc']:+.2f}") print(f" Unrealized PnL: ${report['unrealized_pnl_usdc']:+.2f}") print(f" Trades: {report['trade_count']}") return report def parse_args() -> argparse.Namespace: """Parse CLI arguments for the trading utilities.""" parser = argparse.ArgumentParser(description="Paper Trading Simulator Utilities") parser.add_argument( "--mode", choices=["fast-sweep", "wall-clock", "finalize-summary"], default="fast-sweep", help="Execution mode", ) parser.add_argument("--initial-usdc", type=float, default=100.0, help="Initial USDC capital") parser.add_argument("--duration-hours", type=float, default=1.0, help="Session duration in hours") parser.add_argument("--tick-interval-seconds", type=float, default=60.0, help="Tick interval in seconds") parser.add_argument("--output", help="Output JSON path for fast-sweep or wall-clock mode") parser.add_argument("--heartbeat-log", help="Heartbeat log path for wall-clock or finalize-summary mode") parser.add_argument("--review-log", help="Optional review heartbeat log path") parser.add_argument("--summary-output", help="Summary markdown output path") parser.add_argument("--cycle-dir", help="Cycle directory for summary finalization") parser.add_argument("--reviewer", default="system-generated", help="Reviewer name for summary output") parser.add_argument("--objective-target", type=float, default=30000.0, help="Objective target value") parser.add_argument("--gross-exit-ceiling", type=float, default=37000.0, help="Gross exit ceiling") parser.add_argument("--heartbeat-every-ticks", type=int, default=5, help="Heartbeat write interval") parser.add_argument("--sweep-count", type=int, default=25, help="Monte Carlo path count for fast-sweep") parser.add_argument("--seed", type=int, help="Optional random seed") return parser.parse_args() def main() -> None: """CLI entry point.""" args = parse_args() output_path = Path(args.output) if args.output else None heartbeat_path = Path(args.heartbeat_log) if args.heartbeat_log else None review_log_path = Path(args.review_log) if args.review_log else None summary_output_path = Path(args.summary_output) if args.summary_output else None cycle_dir = Path(args.cycle_dir) if args.cycle_dir else None if args.mode == "fast-sweep": run_fast_monte_carlo_sweep( initial_usdc=args.initial_usdc, duration_hours=args.duration_hours, tick_interval_seconds=args.tick_interval_seconds, sweep_count=args.sweep_count, output_path=output_path, seed=args.seed, ) return if args.mode == "wall-clock": run_wall_clock_session( initial_usdc=args.initial_usdc, duration_hours=args.duration_hours, tick_interval_seconds=args.tick_interval_seconds, heartbeat_every_ticks=args.heartbeat_every_ticks, output_path=output_path, heartbeat_path=heartbeat_path, cycle_dir=cycle_dir, review_log_path=review_log_path, summary_output_path=summary_output_path, reviewer=args.reviewer, objective_target=args.objective_target, gross_exit_ceiling=args.gross_exit_ceiling, seed=args.seed, ) return if not cycle_dir: raise SystemExit("--cycle-dir is required for finalize-summary mode") if not output_path: raise SystemExit("--output is required for finalize-summary mode and must point to the completed wall-clock session report") if not heartbeat_path: raise SystemExit("--heartbeat-log is required for finalize-summary mode") summary_path = finalize_one_page_summary( cycle_dir=cycle_dir, session_report_path=output_path, heartbeat_log_path=heartbeat_path, review_log_path=review_log_path, summary_output_path=summary_output_path, reviewer=args.reviewer, objective_target=args.objective_target, gross_exit_ceiling=args.gross_exit_ceiling, ) print(f"āœ“ One-page summary written to {summary_path}") if __name__ == "__main__": main()