dhruv575 commited on
Commit
91f4bb2
·
1 Parent(s): ed92f24

Implemented

Browse files
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ gcc \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY app.py .
17
+ COPY simulation_utils.py .
18
+ COPY data/ ./data/
19
+ COPY static/ ./static/
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Run the application
25
+ CMD ["python", "app.py"]
26
+
README.md CHANGED
@@ -1,10 +1,81 @@
1
- ---
2
- title: SafeChoicesSimulation
3
- emoji: 🌖
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Safe Choices Simulation - Docker Application
2
+
3
+ A complete Docker-based application for running prediction market simulations using real Polymarket data.
4
+
5
+ ## Structure
6
+
7
+ - **Backend**: FastAPI server (`app.py`) that runs real simulations using `simulation_utils.py`
8
+ - **Frontend**: Static web interface served by FastAPI
9
+ - **Data**: Market data CSV loaded on startup
10
+ - **Docker**: Containerized application ready for deployment
11
+
12
+ ## Files
13
+
14
+ - `Dockerfile` - Docker container definition
15
+ - `app.py` - FastAPI backend server
16
+ - `simulation_utils.py` - Core simulation logic
17
+ - `requirements.txt` - Python dependencies
18
+ - `data/` - Market data CSV file
19
+ - `static/` - Frontend HTML, CSS, JavaScript files
20
+
21
+ ## Building and Running
22
+
23
+ ### Build the Docker image:
24
+ ```bash
25
+ docker build -t safe-choices-simulation .
26
+ ```
27
+
28
+ ### Run the container:
29
+ ```bash
30
+ docker run -p 7860:7860 safe-choices-simulation
31
+ ```
32
+
33
+ The application will be available at `http://localhost:7860`
34
+
35
+ ## API Endpoints
36
+
37
+ - `GET /` - Serves the frontend interface
38
+ - `GET /health` - Health check endpoint
39
+ - `POST /api/simulate` - Run simulations with parameters
40
+
41
+ ### Simulation API Request Format:
42
+ ```json
43
+ {
44
+ "simType": "single" | "threshold" | "multi",
45
+ "startingCapital": 10000,
46
+ "numSimulations": 100,
47
+ "startDate": "2025-01-01",
48
+ "maxDuration": 365,
49
+ "minProb7d": 0.90,
50
+ "minProbCurrent": 0.90,
51
+ "daysBefore": 1,
52
+ "skewFactor": 0.1,
53
+ "targetReturn": 0.0414, // Optional, for threshold type
54
+ "numFunds": 5 // Required for multi type
55
+ }
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Real Data**: Uses actual Polymarket market data from CSV
61
+ - **Three Simulation Types**:
62
+ - Single Fund: All capital in one fund
63
+ - Single Fund Threshold: Stop at target return
64
+ - Multi Fund: Diversified portfolio
65
+ - **Real-time Progress**: Frontend shows simulation progress
66
+ - **Comprehensive Results**: Statistics, charts, and export capabilities
67
+
68
+ ## Development
69
+
70
+ To run locally without Docker:
71
+
72
+ ```bash
73
+ pip install -r requirements.txt
74
+ python app.py
75
+ ```
76
+
77
+ ## Notes
78
+
79
+ - Maximum 100 simulations per request (for performance)
80
+ - Market data is loaded once on startup
81
+ - All simulations use actual historical market probabilities and outcomes
app.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
8
+ from typing import Optional, List, Dict, Any
9
+ import pandas as pd
10
+ import numpy as np
11
+ from simulation_utils import (
12
+ load_and_filter_data,
13
+ run_single_fund_simulation,
14
+ run_multi_fund_simulation
15
+ )
16
+
17
+ # Initialize FastAPI app
18
+ app = FastAPI(title="Safe Choices Simulation API")
19
+
20
+ # Enable CORS
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # Global variable to cache loaded data
30
+ market_data = None
31
+ DATA_PATH = "data/all_filtered_markets_full_2024_2025.csv"
32
+
33
+ # Load market data on startup
34
+ @app.on_event("startup")
35
+ async def load_data():
36
+ global market_data
37
+ if os.path.exists(DATA_PATH):
38
+ print(f"Loading market data from {DATA_PATH}...")
39
+ market_data = load_and_filter_data(DATA_PATH, start_date='2025-01-01')
40
+ print(f"Loaded {len(market_data):,} markets")
41
+ else:
42
+ print(f"Warning: Data file {DATA_PATH} not found!")
43
+
44
+ # Request models
45
+ class SimulationParams(BaseModel):
46
+ simType: str # 'single', 'threshold', or 'multi'
47
+ startingCapital: float
48
+ numSimulations: int
49
+ startDate: str
50
+ maxDuration: int
51
+ minProb7d: float
52
+ minProbCurrent: float
53
+ daysBefore: int
54
+ skewFactor: float
55
+ targetReturn: Optional[float] = None
56
+ numFunds: Optional[int] = None
57
+
58
+ # API Routes
59
+ @app.get("/")
60
+ async def read_root():
61
+ """Serve the frontend index.html"""
62
+ return FileResponse("static/index.html")
63
+
64
+ @app.get("/health")
65
+ async def health_check():
66
+ """Health check endpoint"""
67
+ return {
68
+ "status": "healthy",
69
+ "markets_loaded": len(market_data) if market_data is not None else 0
70
+ }
71
+
72
+ @app.post("/api/simulate")
73
+ async def run_simulation(params: SimulationParams):
74
+ """Run simulation based on parameters"""
75
+ if market_data is None:
76
+ raise HTTPException(status_code=500, detail="Market data not loaded")
77
+
78
+ if params.numSimulations > 100:
79
+ raise HTTPException(status_code=400, detail="Maximum 100 simulations allowed")
80
+
81
+ if params.numSimulations < 1:
82
+ raise HTTPException(status_code=400, detail="At least 1 simulation required")
83
+
84
+ results = {
85
+ "simType": params.simType,
86
+ "parameters": params.dict(),
87
+ "runs": [],
88
+ "summary": {}
89
+ }
90
+
91
+ # Run simulations
92
+ for i in range(params.numSimulations):
93
+ if params.simType == 'multi':
94
+ if params.numFunds is None or params.numFunds < 2:
95
+ raise HTTPException(status_code=400, detail="numFunds required for multi simulation")
96
+
97
+ result = run_multi_fund_simulation(
98
+ df=market_data,
99
+ n_funds=params.numFunds,
100
+ starting_capital=params.startingCapital,
101
+ start_date=params.startDate,
102
+ max_duration_days=params.maxDuration,
103
+ days_before=params.daysBefore,
104
+ min_prob_7d=params.minProb7d,
105
+ min_prob_current=params.minProbCurrent,
106
+ ending_factor_start=0.05,
107
+ ending_factor_increment=0.001,
108
+ skew_factor=params.skewFactor,
109
+ target_return=params.targetReturn,
110
+ random_seed=i
111
+ )
112
+
113
+ # Convert to frontend format
114
+ run = {
115
+ "finalCapital": float(result['portfolio_final_capital']),
116
+ "totalReturn": float(result['portfolio_total_return']),
117
+ "numTrades": int(result['total_trades']),
118
+ "wentBust": bool(result['surviving_funds'] == 0),
119
+ "reachedTarget": bool(result.get('funds_reached_target', 0) > 0),
120
+ "simulationDays": int(max([f['simulation_days'] for f in result['fund_results']]) if result['fund_results'] else 0),
121
+ "survivingFunds": int(result['surviving_funds']),
122
+ "fundResults": [
123
+ {
124
+ "finalCapital": float(f['final_capital']),
125
+ "totalReturn": float(f['total_return']),
126
+ "numTrades": int(f['num_trades']),
127
+ "wentBust": bool(f['went_bust']),
128
+ "reachedTarget": bool(f.get('reached_target', False))
129
+ }
130
+ for f in result['fund_results']
131
+ ],
132
+ "capitalHistory": [] # Simplified for API
133
+ }
134
+
135
+ else:
136
+ # Single fund or threshold
137
+ result = run_single_fund_simulation(
138
+ df=market_data,
139
+ starting_capital=params.startingCapital,
140
+ start_date=params.startDate,
141
+ max_duration_days=params.maxDuration,
142
+ days_before=params.daysBefore,
143
+ min_prob_7d=params.minProb7d,
144
+ min_prob_current=params.minProbCurrent,
145
+ ending_factor_start=0.05,
146
+ ending_factor_increment=0.001,
147
+ skew_factor=params.skewFactor,
148
+ target_return=params.targetReturn,
149
+ random_seed=i
150
+ )
151
+
152
+ # Convert to frontend format
153
+ run = {
154
+ "finalCapital": float(result['final_capital']),
155
+ "totalReturn": float(result['total_return']),
156
+ "numTrades": int(result['num_trades']),
157
+ "wentBust": bool(result['went_bust']),
158
+ "reachedTarget": bool(result.get('reached_target', False)),
159
+ "simulationDays": int(result['simulation_days']),
160
+ "capitalHistory": [
161
+ {"day": int(dc['day']), "capital": float(dc['capital'])}
162
+ for dc in result['daily_capital']
163
+ ]
164
+ }
165
+
166
+ results["runs"].append(run)
167
+
168
+ # Calculate summary statistics
169
+ returns = [r['totalReturn'] for r in results['runs']]
170
+ capitals = [r['finalCapital'] for r in results['runs']]
171
+ trades = [r['numTrades'] for r in results['runs']]
172
+
173
+ results["summary"] = {
174
+ "avgReturn": float(np.mean(returns)),
175
+ "medianReturn": float(np.median(returns)),
176
+ "returnVolatility": float(np.std(returns)),
177
+ "avgFinalCapital": float(np.mean(capitals)),
178
+ "medianFinalCapital": float(np.median(capitals)),
179
+ "bustRate": sum(1 for r in results['runs'] if r['wentBust']) / len(results['runs']),
180
+ "positiveReturnRate": sum(1 for r in returns if r > 0) / len(returns),
181
+ "avgTrades": float(np.mean(trades)),
182
+ "return5th": float(np.percentile(returns, 5)),
183
+ "return95th": float(np.percentile(returns, 95)),
184
+ "maxDrawdown": float(abs(min(returns)))
185
+ }
186
+
187
+ # Type-specific stats
188
+ if params.simType == 'threshold' and params.targetReturn:
189
+ reached_target = sum(1 for r in results['runs'] if r['reachedTarget'])
190
+ results["summary"]["targetReachedRate"] = reached_target / len(results['runs'])
191
+ avg_time = np.mean([r['simulationDays'] for r in results['runs'] if r['reachedTarget']]) if reached_target > 0 else 0
192
+ results["summary"]["avgTimeToTarget"] = float(avg_time)
193
+
194
+ if params.simType == 'multi':
195
+ surviving_funds = [r['survivingFunds'] for r in results['runs']]
196
+ results["summary"]["avgSurvivingFunds"] = float(np.mean(surviving_funds))
197
+ results["summary"]["survivorshipRate"] = float(np.mean(surviving_funds) / params.numFunds)
198
+ results["summary"]["portfolioBustRate"] = sum(1 for s in surviving_funds if s == 0) / len(surviving_funds)
199
+
200
+ return results
201
+
202
+ # Mount static files
203
+ app.mount("/static", StaticFiles(directory="static"), name="static")
204
+
205
+ if __name__ == "__main__":
206
+ import uvicorn
207
+ uvicorn.run(app, host="0.0.0.0", port=7860)
208
+
data/all_filtered_markets_full_2024_2025.csv ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ pandas==2.1.3
4
+ numpy==1.26.2
5
+ scipy==1.11.4
6
+ matplotlib==3.8.2
7
+ seaborn==0.13.0
8
+ python-multipart==0.0.6
9
+
simulation_utils.py ADDED
@@ -0,0 +1,815 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simulation utilities for Safe Choices prediction market trading simulations.
3
+
4
+ This module contains shared functions for running Monte Carlo simulations
5
+ of different trading strategies on prediction markets.
6
+ """
7
+
8
+ import pandas as pd
9
+ import numpy as np
10
+ from datetime import datetime, timedelta
11
+ from scipy import stats
12
+ import matplotlib.pyplot as plt
13
+ import seaborn as sns
14
+ from typing import Tuple, Optional, List, Dict, Any
15
+
16
+ def load_and_filter_data(csv_path: str, start_date: str = '2025-01-01') -> pd.DataFrame:
17
+ """
18
+ Load the market data and filter for simulation period.
19
+
20
+ Args:
21
+ csv_path: Path to the CSV file containing market data
22
+ start_date: Start date for simulation (markets must close after this date)
23
+
24
+ Returns:
25
+ Filtered DataFrame ready for simulation
26
+ """
27
+ # Load data
28
+ df = pd.read_csv(csv_path)
29
+
30
+ # Convert dates - handle timezone awareness
31
+ df['closingDate'] = pd.to_datetime(df['closingDate'], format='mixed', errors='coerce', utc=True)
32
+ start_dt = pd.to_datetime(start_date, utc=True)
33
+
34
+ # Filter for markets that close after start date and have complete data
35
+ mask = (
36
+ (df['closingDate'] >= start_dt) &
37
+ (df['outcome'].notna()) &
38
+ (df['probability7d'].notna()) &
39
+ (df['probability6d'].notna()) &
40
+ (df['probability5d'].notna()) &
41
+ (df['probability4d'].notna()) &
42
+ (df['probability3d'].notna()) &
43
+ (df['probability2d'].notna()) &
44
+ (df['probability1d'].notna())
45
+ )
46
+
47
+ filtered_df = df[mask].copy().reset_index(drop=True)
48
+
49
+ # Vectorized outcome conversion for speed
50
+ outcome_map = {'True': 1, 'true': 1, 'FALSE': 0, 'false': 0, True: 1, False: 0}
51
+ filtered_df['outcome_int'] = filtered_df['outcome'].map(outcome_map)
52
+
53
+ # Fill any remaining NaN outcomes with proper conversion
54
+ remaining_mask = filtered_df['outcome_int'].isna()
55
+ if remaining_mask.any():
56
+ def convert_outcome(value):
57
+ if pd.isna(value):
58
+ return None
59
+ if isinstance(value, (int, float)):
60
+ return int(value)
61
+ return 1 if str(value).lower() == 'true' else 0
62
+
63
+ filtered_df.loc[remaining_mask, 'outcome_int'] = filtered_df.loc[remaining_mask, 'outcome'].apply(convert_outcome)
64
+
65
+ # Sort by closing date for better performance in simulations
66
+ filtered_df = filtered_df.sort_values('closingDate').reset_index(drop=True)
67
+
68
+ return filtered_df
69
+
70
+ def check_market_eligibility(market_row: pd.Series, days_before: int,
71
+ min_prob_7d: float, min_prob_current: float) -> bool:
72
+ """
73
+ Check if a market meets the probability thresholds for investment.
74
+
75
+ Args:
76
+ market_row: Row from the DataFrame containing market data
77
+ days_before: Number of days before resolution to check (1-7)
78
+ min_prob_7d: Minimum probability threshold at 7 days before
79
+ min_prob_current: Minimum probability threshold at current day
80
+
81
+ Returns:
82
+ True if market meets criteria, False otherwise
83
+ """
84
+ prob_col = f'probability{days_before}d'
85
+
86
+ # Check if required columns exist and have valid data
87
+ if pd.isna(market_row.get('probability7d')) or pd.isna(market_row.get(prob_col)):
88
+ return False
89
+
90
+ # Check probability thresholds
91
+ prob_7d = market_row['probability7d']
92
+ prob_current = market_row[prob_col]
93
+
94
+ return prob_7d >= min_prob_7d and prob_current >= min_prob_current
95
+
96
+ def calculate_days_until_resolution(current_date: datetime, closing_date: datetime) -> int:
97
+ """
98
+ Calculate days until market resolution.
99
+
100
+ Args:
101
+ current_date: Current simulation date
102
+ closing_date: Market closing date
103
+
104
+ Returns:
105
+ Number of days until resolution
106
+ """
107
+ return max(0, (closing_date - current_date).days)
108
+
109
+ def get_available_markets(df: pd.DataFrame, current_date: datetime, days_before: int,
110
+ min_prob_7d: float, min_prob_current: float) -> pd.DataFrame:
111
+ """
112
+ Get markets available for investment at current date.
113
+
114
+ Args:
115
+ df: DataFrame containing market data
116
+ current_date: Current simulation date
117
+ days_before: Days before resolution to invest
118
+ min_prob_7d: Minimum probability at 7 days before
119
+ min_prob_current: Minimum probability at current day
120
+
121
+ Returns:
122
+ DataFrame of available markets with their days until resolution
123
+ """
124
+ # Vectorized approach - much faster than iterating
125
+ prob_col = f'probability{days_before}d'
126
+
127
+ # Calculate days until resolution for all markets at once
128
+ days_until = (df['closingDate'] - current_date).dt.days
129
+
130
+ # Create boolean mask for all conditions at once
131
+ mask = (
132
+ (days_until >= days_before) & # Market resolves in future
133
+ (df['probability7d'] >= min_prob_7d) & # 7d probability threshold
134
+ (df[prob_col] >= min_prob_current) & # Current day probability threshold
135
+ (df['probability7d'].notna()) & # Valid 7d data
136
+ (df[prob_col].notna()) & # Valid current day data
137
+ (df['outcome_int'].notna()) # Valid outcome data
138
+ )
139
+
140
+ if not mask.any():
141
+ return pd.DataFrame()
142
+
143
+ # Filter and add days_until_resolution column
144
+ available_markets = df[mask].copy()
145
+ available_markets['days_until_resolution'] = days_until[mask]
146
+
147
+ return available_markets
148
+
149
+ def select_next_market(available_markets: pd.DataFrame, random_state: np.random.RandomState,
150
+ skew_factor: float = 0.1) -> Optional[pd.Series]:
151
+ """
152
+ Select the next market to invest in using a left-skewed exponential distribution.
153
+
154
+ Args:
155
+ available_markets: DataFrame of markets available for investment
156
+ random_state: Random state for reproducible results
157
+ skew_factor: Controls the left skew (higher = more skew toward closer markets)
158
+
159
+ Returns:
160
+ Selected market as Series, or None if no markets available
161
+ """
162
+ if len(available_markets) == 0:
163
+ return None
164
+
165
+ # Configurable market selection with adjustable skew
166
+ days_until = available_markets['days_until_resolution'].values
167
+
168
+ # Use exponential decay with configurable skew factor
169
+ # Closer markets (lower days) get exponentially higher weights
170
+ weights = np.exp(-days_until * skew_factor)
171
+
172
+ # Normalize weights
173
+ weights = weights / weights.sum()
174
+
175
+ # Select market based on weights
176
+ selected_idx = random_state.choice(len(available_markets), p=weights)
177
+
178
+ return available_markets.iloc[selected_idx]
179
+
180
+ def calculate_investment_return(market: pd.Series, days_before: int, capital: float) -> float:
181
+ """
182
+ Calculate return from investing in a market.
183
+
184
+ Args:
185
+ market: Market data as Series
186
+ days_before: Days before resolution when investment was made
187
+ capital: Amount invested
188
+
189
+ Returns:
190
+ Final capital after resolution (capital * (1/probability) if win, 0 if loss)
191
+ """
192
+ prob_col = f'probability{days_before}d'
193
+ probability = market[prob_col]
194
+ outcome = market['outcome_int']
195
+
196
+ if outcome == 1: # Market resolved True
197
+ # Return is capital * (1 / probability)
198
+ return capital / probability
199
+ else: # Market resolved False
200
+ return 0.0
201
+
202
+ def run_single_fund_simulation(df: pd.DataFrame,
203
+ starting_capital: float = 10000,
204
+ start_date: str = '2025-01-01',
205
+ max_duration_days: int = 365,
206
+ days_before: int = 1,
207
+ min_prob_7d: float = 0.90,
208
+ min_prob_current: float = 0.90,
209
+ ending_factor_start: float = 0.05,
210
+ ending_factor_increment: float = 0.001,
211
+ skew_factor: float = 0.1,
212
+ target_return: Optional[float] = None,
213
+ random_seed: Optional[int] = None) -> Dict[str, Any]:
214
+ """
215
+ Run a single fund simulation.
216
+
217
+ Args:
218
+ df: Market data DataFrame
219
+ starting_capital: Initial capital
220
+ start_date: Simulation start date
221
+ max_duration_days: Maximum simulation duration
222
+ days_before: Days before resolution to invest
223
+ min_prob_7d: Minimum probability at 7 days
224
+ min_prob_current: Minimum probability at investment day
225
+ ending_factor_start: Starting ending factor
226
+ ending_factor_increment: Daily increment to ending factor
227
+ skew_factor: Controls left skew in market selection (higher = more skew)
228
+ target_return: Target return threshold to stop trading (None = no threshold)
229
+ random_seed: Random seed for reproducibility
230
+
231
+ Returns:
232
+ Dictionary containing simulation results
233
+ """
234
+ random_state = np.random.RandomState(random_seed)
235
+
236
+ current_date = pd.to_datetime(start_date, utc=True)
237
+ end_date = current_date + timedelta(days=max_duration_days)
238
+ capital = starting_capital
239
+
240
+ trades = []
241
+ daily_capital = []
242
+
243
+ sim_day = 0
244
+
245
+ # Track daily capital less frequently to improve performance
246
+ last_capital_record = 0
247
+
248
+ while current_date <= end_date and capital > 0:
249
+ # Check if we've reached target return threshold
250
+ if target_return is not None:
251
+ current_return = (capital - starting_capital) / starting_capital
252
+ if current_return >= target_return:
253
+ break
254
+
255
+ # Calculate current ending factor
256
+ ending_factor = ending_factor_start + (ending_factor_increment * sim_day)
257
+
258
+ # Check if we hit ending factor
259
+ if random_state.random() < ending_factor:
260
+ break
261
+
262
+ # Get available markets
263
+ available_markets = get_available_markets(
264
+ df, current_date, days_before, min_prob_7d, min_prob_current
265
+ )
266
+
267
+ if len(available_markets) == 0:
268
+ # No markets available, advance day by larger steps to speed up
269
+ current_date += timedelta(days=7) # Skip a week instead of one day
270
+ sim_day = (current_date - pd.to_datetime(start_date, utc=True)).days
271
+ continue
272
+
273
+ # Select market
274
+ selected_market = select_next_market(available_markets, random_state, skew_factor)
275
+
276
+ if selected_market is None:
277
+ current_date += timedelta(days=7)
278
+ sim_day = (current_date - pd.to_datetime(start_date, utc=True)).days
279
+ continue
280
+
281
+ # Calculate investment date and resolution date
282
+ investment_date = current_date
283
+ resolution_date = selected_market['closingDate']
284
+
285
+ # Calculate return
286
+ new_capital = calculate_investment_return(selected_market, days_before, capital)
287
+
288
+ # Record trade (only essential info for speed)
289
+ trades.append({
290
+ 'trade_number': len(trades) + 1,
291
+ 'investment_date': investment_date,
292
+ 'resolution_date': resolution_date,
293
+ 'probability': selected_market[f'probability{days_before}d'],
294
+ 'capital_invested': capital,
295
+ 'outcome': selected_market['outcome_int'],
296
+ 'capital_after': new_capital,
297
+ 'return': (new_capital - capital) / capital if capital > 0 else 0,
298
+ 'sim_day': sim_day
299
+ })
300
+
301
+ capital = new_capital
302
+
303
+ # Advance to resolution date + 1 day
304
+ current_date = resolution_date + timedelta(days=1)
305
+ sim_day = (current_date - pd.to_datetime(start_date, utc=True)).days
306
+
307
+ # Record daily capital less frequently (every 10th day or trade)
308
+ if sim_day - last_capital_record >= 10 or len(trades) % 10 == 0:
309
+ daily_capital.append({
310
+ 'date': current_date,
311
+ 'capital': capital,
312
+ 'day': sim_day
313
+ })
314
+ last_capital_record = sim_day
315
+
316
+ # Calculate final statistics
317
+ total_return = (capital - starting_capital) / starting_capital if starting_capital > 0 else 0
318
+ num_trades = len(trades)
319
+ went_bust = capital == 0
320
+ reached_target = target_return is not None and total_return >= target_return
321
+
322
+ # Determine ending reason
323
+ if went_bust:
324
+ ending_reason = 'bust'
325
+ elif reached_target:
326
+ ending_reason = 'target_reached'
327
+ elif current_date <= end_date:
328
+ ending_reason = 'ending_factor'
329
+ else:
330
+ ending_reason = 'max_duration'
331
+
332
+ return {
333
+ 'final_capital': capital,
334
+ 'total_return': total_return,
335
+ 'num_trades': num_trades,
336
+ 'went_bust': went_bust,
337
+ 'reached_target': reached_target,
338
+ 'ending_reason': ending_reason,
339
+ 'simulation_days': sim_day,
340
+ 'trades': trades,
341
+ 'daily_capital': daily_capital,
342
+ 'parameters': {
343
+ 'starting_capital': starting_capital,
344
+ 'start_date': start_date,
345
+ 'max_duration_days': max_duration_days,
346
+ 'days_before': days_before,
347
+ 'min_prob_7d': min_prob_7d,
348
+ 'min_prob_current': min_prob_current,
349
+ 'ending_factor_start': ending_factor_start,
350
+ 'ending_factor_increment': ending_factor_increment,
351
+ 'skew_factor': skew_factor,
352
+ 'target_return': target_return,
353
+ 'random_seed': random_seed
354
+ }
355
+ }
356
+
357
+ def plot_simulation_results(results_list: List[Dict[str, Any]], title: str = "Simulation Results"):
358
+ """
359
+ Plot results from multiple simulation runs.
360
+
361
+ Args:
362
+ results_list: List of simulation result dictionaries
363
+ title: Plot title
364
+ """
365
+ if not results_list:
366
+ print("No results to plot")
367
+ return
368
+
369
+ # Extract data
370
+ final_capitals = [r['final_capital'] for r in results_list]
371
+ total_returns = [r['total_return'] for r in results_list]
372
+ num_trades = [r['num_trades'] for r in results_list]
373
+ bust_rate = sum(1 for r in results_list if r['went_bust']) / len(results_list)
374
+
375
+ # Create plots
376
+ fig, axes = plt.subplots(2, 2, figsize=(15, 12))
377
+
378
+ # Final capital distribution
379
+ axes[0, 0].hist(final_capitals, bins=50, alpha=0.7, edgecolor='black')
380
+ axes[0, 0].axvline(np.mean(final_capitals), color='red', linestyle='--',
381
+ label=f'Mean: ${np.mean(final_capitals):,.0f}')
382
+ axes[0, 0].axvline(np.median(final_capitals), color='green', linestyle='--',
383
+ label=f'Median: ${np.median(final_capitals):,.0f}')
384
+ axes[0, 0].set_xlabel('Final Capital ($)')
385
+ axes[0, 0].set_ylabel('Frequency')
386
+ axes[0, 0].set_title('Final Capital Distribution')
387
+ axes[0, 0].legend()
388
+ axes[0, 0].grid(True, alpha=0.3)
389
+
390
+ # Return distribution
391
+ return_pct = [r * 100 for r in total_returns]
392
+ axes[0, 1].hist(return_pct, bins=50, alpha=0.7, edgecolor='black')
393
+ axes[0, 1].axvline(np.mean(return_pct), color='red', linestyle='--',
394
+ label=f'Mean: {np.mean(return_pct):.1f}%')
395
+ axes[0, 1].axvline(np.median(return_pct), color='green', linestyle='--',
396
+ label=f'Median: {np.median(return_pct):.1f}%')
397
+ axes[0, 1].axvline(0, color='black', linestyle='-', alpha=0.5, label='Break-even')
398
+ axes[0, 1].set_xlabel('Total Return (%)')
399
+ axes[0, 1].set_ylabel('Frequency')
400
+ axes[0, 1].set_title('Return Distribution')
401
+ axes[0, 1].legend()
402
+ axes[0, 1].grid(True, alpha=0.3)
403
+
404
+ # Number of trades distribution
405
+ axes[1, 0].hist(num_trades, bins=30, alpha=0.7, edgecolor='black')
406
+ axes[1, 0].axvline(np.mean(num_trades), color='red', linestyle='--',
407
+ label=f'Mean: {np.mean(num_trades):.1f}')
408
+ axes[1, 0].set_xlabel('Number of Trades')
409
+ axes[1, 0].set_ylabel('Frequency')
410
+ axes[1, 0].set_title('Number of Trades Distribution')
411
+ axes[1, 0].legend()
412
+ axes[1, 0].grid(True, alpha=0.3)
413
+
414
+ # Summary statistics
415
+ axes[1, 1].axis('off')
416
+ stats_text = f"""
417
+ Summary Statistics:
418
+
419
+ Total Simulations: {len(results_list):,}
420
+ Bust Rate: {bust_rate:.1%}
421
+
422
+ Final Capital:
423
+ Mean: ${np.mean(final_capitals):,.0f}
424
+ Median: ${np.median(final_capitals):,.0f}
425
+ Min: ${np.min(final_capitals):,.0f}
426
+ Max: ${np.max(final_capitals):,.0f}
427
+
428
+ Total Return:
429
+ Mean: {np.mean(total_returns):.1%}
430
+ Median: {np.median(total_returns):.1%}
431
+ Min: {np.min(total_returns):.1%}
432
+ Max: {np.max(total_returns):.1%}
433
+
434
+ Trades per Simulation:
435
+ Mean: {np.mean(num_trades):.1f}
436
+ Median: {np.median(num_trades):.1f}
437
+ """
438
+
439
+ axes[1, 1].text(0.1, 0.9, stats_text, transform=axes[1, 1].transAxes,
440
+ fontsize=11, verticalalignment='top', fontfamily='monospace')
441
+
442
+ plt.suptitle(title, fontsize=16, fontweight='bold')
443
+ plt.tight_layout()
444
+ plt.show()
445
+
446
+ def print_simulation_summary(results_list: List[Dict[str, Any]]):
447
+ """
448
+ Print detailed summary statistics for simulation results.
449
+
450
+ Args:
451
+ results_list: List of simulation result dictionaries
452
+ """
453
+ if not results_list:
454
+ print("No results to summarize")
455
+ return
456
+
457
+ # Extract data
458
+ final_capitals = np.array([r['final_capital'] for r in results_list])
459
+ total_returns = np.array([r['total_return'] for r in results_list])
460
+ num_trades = np.array([r['num_trades'] for r in results_list])
461
+
462
+ # Calculate statistics
463
+ bust_count = sum(1 for r in results_list if r['went_bust'])
464
+ bust_rate = bust_count / len(results_list)
465
+
466
+ target_reached_count = sum(1 for r in results_list if r.get('reached_target', False))
467
+ target_reached_rate = target_reached_count / len(results_list)
468
+
469
+ positive_return_count = sum(1 for r in total_returns if r > 0)
470
+ positive_return_rate = positive_return_count / len(results_list)
471
+
472
+ # Check if target return was used
473
+ target_return = results_list[0]['parameters'].get('target_return', None)
474
+
475
+ print("=" * 60)
476
+ print("SIMULATION SUMMARY")
477
+ print("=" * 60)
478
+ print(f"Total Simulations: {len(results_list):,}")
479
+ print(f"Went Bust: {bust_count:,} ({bust_rate:.1%})")
480
+ if target_return is not None:
481
+ print(f"Reached Target ({target_return:.1%}): {target_reached_count:,} ({target_reached_rate:.1%})")
482
+ print(f"Positive Returns: {positive_return_count:,} ({positive_return_rate:.1%})")
483
+
484
+ print(f"\nFINAL CAPITAL STATISTICS:")
485
+ print(f"Mean: ${final_capitals.mean():,.2f}")
486
+ print(f"Median: ${np.median(final_capitals):,.2f}")
487
+ print(f"Std Dev: ${final_capitals.std():,.2f}")
488
+ print(f"Min: ${final_capitals.min():,.2f}")
489
+ print(f"Max: ${final_capitals.max():,.2f}")
490
+
491
+ print(f"\nRETURN STATISTICS:")
492
+ print(f"Mean: {total_returns.mean():.1%}")
493
+ print(f"Median: {np.median(total_returns):.1%}")
494
+ print(f"Std Dev: {total_returns.std():.1%}")
495
+ print(f"Min: {total_returns.min():.1%}")
496
+ print(f"Max: {total_returns.max():.1%}")
497
+
498
+ print(f"\nTRADE STATISTICS:")
499
+ print(f"Mean Trades: {num_trades.mean():.1f}")
500
+ print(f"Median Trades: {np.median(num_trades):.1f}")
501
+ print(f"Min Trades: {num_trades.min()}")
502
+ print(f"Max Trades: {num_trades.max()}")
503
+
504
+ # Percentiles
505
+ percentiles = [5, 10, 25, 75, 90, 95]
506
+ print(f"\nRETURN PERCENTILES:")
507
+ for p in percentiles:
508
+ value = np.percentile(total_returns, p)
509
+ print(f"{p}th percentile: {value:.1%}")
510
+
511
+ def run_multi_fund_simulation(df: pd.DataFrame,
512
+ n_funds: int = 5,
513
+ starting_capital: float = 10000,
514
+ start_date: str = '2025-01-01',
515
+ max_duration_days: int = 365,
516
+ days_before: int = 1,
517
+ min_prob_7d: float = 0.90,
518
+ min_prob_current: float = 0.90,
519
+ ending_factor_start: float = 0.05,
520
+ ending_factor_increment: float = 0.001,
521
+ skew_factor: float = 0.1,
522
+ target_return: Optional[float] = None,
523
+ random_seed: Optional[int] = None) -> Dict[str, Any]:
524
+ """
525
+ Run a multi-fund simulation where capital is divided into independent funds.
526
+
527
+ Args:
528
+ df: Market data DataFrame
529
+ n_funds: Number of independent funds to create
530
+ starting_capital: Total initial capital (divided among funds)
531
+ start_date: Simulation start date
532
+ max_duration_days: Maximum simulation duration
533
+ days_before: Days before resolution to invest
534
+ min_prob_7d: Minimum probability at 7 days
535
+ min_prob_current: Minimum probability at investment day
536
+ ending_factor_start: Starting ending factor
537
+ ending_factor_increment: Daily increment to ending factor
538
+ skew_factor: Controls left skew in market selection
539
+ target_return: Target return threshold per fund (None = no threshold)
540
+ random_seed: Random seed for reproducibility
541
+
542
+ Returns:
543
+ Dictionary containing multi-fund simulation results
544
+ """
545
+ # Set up random state
546
+ random_state = np.random.RandomState(random_seed)
547
+
548
+ # Calculate capital per fund
549
+ capital_per_fund = starting_capital / n_funds
550
+
551
+ # Run simulation for each fund independently
552
+ fund_results = []
553
+ all_trades = []
554
+
555
+ for fund_id in range(n_funds):
556
+ # Use different seed for each fund to ensure independence
557
+ fund_seed = random_state.randint(0, 1000000)
558
+
559
+ # Run single fund simulation for this fund
560
+ fund_result = run_single_fund_simulation(
561
+ df=df,
562
+ starting_capital=capital_per_fund,
563
+ start_date=start_date,
564
+ max_duration_days=max_duration_days,
565
+ days_before=days_before,
566
+ min_prob_7d=min_prob_7d,
567
+ min_prob_current=min_prob_current,
568
+ ending_factor_start=ending_factor_start,
569
+ ending_factor_increment=ending_factor_increment,
570
+ skew_factor=skew_factor,
571
+ target_return=target_return,
572
+ random_seed=fund_seed
573
+ )
574
+
575
+ # Add fund ID to result and trades
576
+ fund_result['fund_id'] = fund_id
577
+ for trade in fund_result['trades']:
578
+ trade['fund_id'] = fund_id
579
+ all_trades.append(trade)
580
+
581
+ fund_results.append(fund_result)
582
+
583
+ # Calculate portfolio-level statistics
584
+ surviving_funds = sum(1 for fund in fund_results if not fund['went_bust'])
585
+ total_final_capital = sum(fund['final_capital'] for fund in fund_results)
586
+ total_portfolio_return = (total_final_capital - starting_capital) / starting_capital if starting_capital > 0 else 0
587
+
588
+ # Calculate average metrics across surviving funds
589
+ if surviving_funds > 0:
590
+ avg_capital_per_surviving_fund = sum(fund['final_capital'] for fund in fund_results if not fund['went_bust']) / surviving_funds
591
+ avg_return_per_surviving_fund = sum(fund['total_return'] for fund in fund_results if not fund['went_bust']) / surviving_funds
592
+ else:
593
+ avg_capital_per_surviving_fund = 0
594
+ avg_return_per_surviving_fund = -1 # All funds went bust
595
+
596
+ # Target achievement stats
597
+ funds_reached_target = sum(1 for fund in fund_results if fund.get('reached_target', False))
598
+ target_achievement_rate = funds_reached_target / n_funds
599
+
600
+ # Trading activity stats
601
+ total_trades = len(all_trades)
602
+ avg_trades_per_fund = total_trades / n_funds
603
+
604
+ # Survivorship and diversification metrics
605
+ survivorship_rate = surviving_funds / n_funds
606
+ bust_rate = 1 - survivorship_rate
607
+
608
+ return {
609
+ 'portfolio_final_capital': total_final_capital,
610
+ 'portfolio_total_return': total_portfolio_return,
611
+ 'n_funds': n_funds,
612
+ 'surviving_funds': surviving_funds,
613
+ 'survivorship_rate': survivorship_rate,
614
+ 'bust_rate': bust_rate,
615
+ 'avg_capital_per_surviving_fund': avg_capital_per_surviving_fund,
616
+ 'avg_return_per_surviving_fund': avg_return_per_surviving_fund,
617
+ 'funds_reached_target': funds_reached_target,
618
+ 'target_achievement_rate': target_achievement_rate,
619
+ 'total_trades': total_trades,
620
+ 'avg_trades_per_fund': avg_trades_per_fund,
621
+ 'fund_results': fund_results,
622
+ 'all_trades': all_trades,
623
+ 'parameters': {
624
+ 'n_funds': n_funds,
625
+ 'starting_capital': starting_capital,
626
+ 'capital_per_fund': capital_per_fund,
627
+ 'start_date': start_date,
628
+ 'max_duration_days': max_duration_days,
629
+ 'days_before': days_before,
630
+ 'min_prob_7d': min_prob_7d,
631
+ 'min_prob_current': min_prob_current,
632
+ 'ending_factor_start': ending_factor_start,
633
+ 'ending_factor_increment': ending_factor_increment,
634
+ 'skew_factor': skew_factor,
635
+ 'target_return': target_return,
636
+ 'random_seed': random_seed
637
+ }
638
+ }
639
+
640
+ def plot_multi_fund_results(results_list: List[Dict[str, Any]], title: str = "Multi-Fund Simulation Results"):
641
+ """
642
+ Plot results from multiple multi-fund simulation runs.
643
+
644
+ Args:
645
+ results_list: List of multi-fund simulation result dictionaries
646
+ title: Plot title
647
+ """
648
+ if not results_list:
649
+ print("No results to plot")
650
+ return
651
+
652
+ # Extract portfolio-level data
653
+ portfolio_final_capitals = [r['portfolio_final_capital'] for r in results_list]
654
+ portfolio_returns = [r['portfolio_total_return'] for r in results_list]
655
+ surviving_funds = [r['surviving_funds'] for r in results_list]
656
+ survivorship_rates = [r['survivorship_rate'] for r in results_list]
657
+
658
+ # Create plots
659
+ fig, axes = plt.subplots(2, 2, figsize=(16, 12))
660
+
661
+ # Portfolio final capital distribution
662
+ axes[0, 0].hist(portfolio_final_capitals, bins=30, alpha=0.7, edgecolor='black', color='steelblue')
663
+ axes[0, 0].axvline(np.mean(portfolio_final_capitals), color='red', linestyle='--',
664
+ label=f'Mean: ${np.mean(portfolio_final_capitals):,.0f}')
665
+ axes[0, 0].axvline(np.median(portfolio_final_capitals), color='green', linestyle='--',
666
+ label=f'Median: ${np.median(portfolio_final_capitals):,.0f}')
667
+ axes[0, 0].set_xlabel('Portfolio Final Capital ($)')
668
+ axes[0, 0].set_ylabel('Frequency')
669
+ axes[0, 0].set_title('Portfolio Final Capital Distribution')
670
+ axes[0, 0].legend()
671
+ axes[0, 0].grid(True, alpha=0.3)
672
+
673
+ # Portfolio return distribution
674
+ return_pct = [r * 100 for r in portfolio_returns]
675
+ axes[0, 1].hist(return_pct, bins=30, alpha=0.7, edgecolor='black', color='green')
676
+ axes[0, 1].axvline(np.mean(return_pct), color='red', linestyle='--',
677
+ label=f'Mean: {np.mean(return_pct):.1f}%')
678
+ axes[0, 1].axvline(0, color='black', linestyle='-', alpha=0.5, label='Break-even')
679
+ axes[0, 1].set_xlabel('Portfolio Total Return (%)')
680
+ axes[0, 1].set_ylabel('Frequency')
681
+ axes[0, 1].set_title('Portfolio Return Distribution')
682
+ axes[0, 1].legend()
683
+ axes[0, 1].grid(True, alpha=0.3)
684
+
685
+ # Number of surviving funds distribution
686
+ n_funds = results_list[0]['n_funds']
687
+ axes[1, 0].hist(surviving_funds, bins=range(n_funds + 2), alpha=0.7, edgecolor='black', color='orange')
688
+ axes[1, 0].axvline(np.mean(surviving_funds), color='red', linestyle='--',
689
+ label=f'Mean: {np.mean(surviving_funds):.1f}')
690
+ axes[1, 0].set_xlabel('Number of Surviving Funds')
691
+ axes[1, 0].set_ylabel('Frequency')
692
+ axes[1, 0].set_title('Surviving Funds Distribution')
693
+ axes[1, 0].legend()
694
+ axes[1, 0].grid(True, alpha=0.3)
695
+ axes[1, 0].set_xticks(range(n_funds + 1))
696
+
697
+ # Summary statistics
698
+ axes[1, 1].axis('off')
699
+
700
+ # Calculate additional stats
701
+ total_bust_rate = sum(1 for r in results_list if r['surviving_funds'] == 0) / len(results_list)
702
+ avg_survivorship = np.mean(survivorship_rates)
703
+
704
+ stats_text = f"""
705
+ Multi-Fund Summary Statistics:
706
+
707
+ Total Simulations: {len(results_list):,}
708
+ Funds per Portfolio: {n_funds}
709
+ Total Bust Rate: {total_bust_rate:.1%}
710
+
711
+ Portfolio Capital:
712
+ Mean: ${np.mean(portfolio_final_capitals):,.0f}
713
+ Median: ${np.median(portfolio_final_capitals):,.0f}
714
+ Min: ${np.min(portfolio_final_capitals):,.0f}
715
+ Max: ${np.max(portfolio_final_capitals):,.0f}
716
+
717
+ Portfolio Return:
718
+ Mean: {np.mean(portfolio_returns):.1%}
719
+ Median: {np.median(portfolio_returns):.1%}
720
+
721
+ Fund Survivorship:
722
+ Avg Surviving: {np.mean(surviving_funds):.1f} / {n_funds}
723
+ Avg Survivorship: {avg_survivorship:.1%}
724
+ """
725
+
726
+ axes[1, 1].text(0.1, 0.9, stats_text, transform=axes[1, 1].transAxes,
727
+ fontsize=11, verticalalignment='top', fontfamily='monospace')
728
+
729
+ plt.suptitle(title, fontsize=16, fontweight='bold')
730
+ plt.tight_layout()
731
+ plt.show()
732
+
733
+ def print_multi_fund_summary(results_list: List[Dict[str, Any]]):
734
+ """
735
+ Print detailed summary statistics for multi-fund simulation results.
736
+
737
+ Args:
738
+ results_list: List of multi-fund simulation result dictionaries
739
+ """
740
+ if not results_list:
741
+ print("No results to summarize")
742
+ return
743
+
744
+ # Extract data
745
+ n_funds = results_list[0]['n_funds']
746
+ portfolio_capitals = np.array([r['portfolio_final_capital'] for r in results_list])
747
+ portfolio_returns = np.array([r['portfolio_total_return'] for r in results_list])
748
+ surviving_funds = np.array([r['surviving_funds'] for r in results_list])
749
+ survivorship_rates = np.array([r['survivorship_rate'] for r in results_list])
750
+
751
+ # Calculate portfolio-level statistics
752
+ total_bust_count = sum(1 for r in results_list if r['surviving_funds'] == 0)
753
+ total_bust_rate = total_bust_count / len(results_list)
754
+
755
+ positive_return_count = sum(1 for r in portfolio_returns if r > 0)
756
+ positive_return_rate = positive_return_count / len(results_list)
757
+
758
+ # Check if target return was used
759
+ target_return = results_list[0]['parameters'].get('target_return', None)
760
+
761
+ print("=" * 80)
762
+ print("MULTI-FUND SIMULATION SUMMARY")
763
+ print("=" * 80)
764
+ print(f"Total Simulations: {len(results_list):,}")
765
+ print(f"Funds per Portfolio: {n_funds}")
766
+ print(f"Starting Capital per Fund: ${results_list[0]['parameters']['capital_per_fund']:,.0f}")
767
+ print(f"Total Starting Capital: ${results_list[0]['parameters']['starting_capital']:,.0f}")
768
+
769
+ print(f"\nPORTFOLIO SURVIVORSHIP:")
770
+ print(f"Total Portfolio Bust Rate: {total_bust_rate:.1%} ({total_bust_count:,} portfolios)")
771
+ print(f"Average Surviving Funds: {surviving_funds.mean():.1f} / {n_funds}")
772
+ print(f"Average Survivorship Rate: {survivorship_rates.mean():.1%}")
773
+ print(f"Portfolios with All Funds Surviving: {sum(1 for s in surviving_funds if s == n_funds)} ({sum(1 for s in surviving_funds if s == n_funds)/len(results_list):.1%})")
774
+
775
+ if target_return is not None:
776
+ target_achieved_portfolios = sum(1 for r in results_list if r['funds_reached_target'] > 0)
777
+ avg_funds_reaching_target = np.mean([r['funds_reached_target'] for r in results_list])
778
+ print(f"\nTARGET ACHIEVEMENT ({target_return:.1%}):")
779
+ print(f"Portfolios with ≥1 Fund Reaching Target: {target_achieved_portfolios:,} ({target_achieved_portfolios/len(results_list):.1%})")
780
+ print(f"Average Funds Reaching Target: {avg_funds_reaching_target:.1f} / {n_funds}")
781
+
782
+ print(f"\nPORTFOLIO PERFORMANCE:")
783
+ print(f"Positive Returns: {positive_return_count:,} ({positive_return_rate:.1%})")
784
+
785
+ print(f"\nPORTFOLIO CAPITAL STATISTICS:")
786
+ print(f"Mean: ${portfolio_capitals.mean():,.2f}")
787
+ print(f"Median: ${np.median(portfolio_capitals):,.2f}")
788
+ print(f"Std Dev: ${portfolio_capitals.std():,.2f}")
789
+ print(f"Min: ${portfolio_capitals.min():,.2f}")
790
+ print(f"Max: ${portfolio_capitals.max():,.2f}")
791
+
792
+ print(f"\nPORTFOLIO RETURN STATISTICS:")
793
+ print(f"Mean: {portfolio_returns.mean():.1%}")
794
+ print(f"Median: {np.median(portfolio_returns):.1%}")
795
+ print(f"Std Dev: {portfolio_returns.std():.1%}")
796
+ print(f"Min: {portfolio_returns.min():.1%}")
797
+ print(f"Max: {portfolio_returns.max():.1%}")
798
+
799
+ # Compare to single fund equivalent
800
+ print(f"\nDIVERSIFICATION ANALYSIS:")
801
+ single_fund_equivalent = results_list[0]['parameters']['starting_capital']
802
+ avg_portfolio_capital = portfolio_capitals.mean()
803
+ diversification_benefit = (avg_portfolio_capital - single_fund_equivalent) / single_fund_equivalent
804
+ print(f"Diversification Benefit: {diversification_benefit:+.1%} vs single fund baseline")
805
+
806
+ # Risk metrics
807
+ portfolio_volatility = portfolio_returns.std()
808
+ print(f"Portfolio Return Volatility: {portfolio_volatility:.1%}")
809
+
810
+ # Percentiles
811
+ percentiles = [5, 10, 25, 75, 90, 95]
812
+ print(f"\nPORTFOLIO RETURN PERCENTILES:")
813
+ for p in percentiles:
814
+ value = np.percentile(portfolio_returns, p)
815
+ print(f"{p}th percentile: {value:.1%}")
static/.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules/
2
+ .DS_Store
3
+ *.log
4
+
static/README.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Safe Choices Frontend
2
+
3
+ Frontend interface for the Safe Choices prediction market simulation tool.
4
+
5
+ ## Setup
6
+
7
+ 1. Navigate to the frontend directory:
8
+ ```bash
9
+ cd frontend
10
+ ```
11
+
12
+ 2. Install dependencies (optional - uses npx, so no install needed):
13
+ ```bash
14
+ npm install
15
+ ```
16
+
17
+ ## Running
18
+
19
+ Start the development server:
20
+ ```bash
21
+ npm run start
22
+ ```
23
+
24
+ This will:
25
+ - Start a local HTTP server on port 3000
26
+ - Automatically open the browser to the application
27
+ - Serve the static files with proper MIME types
28
+
29
+ For development with auto-reload (no caching):
30
+ ```bash
31
+ npm run dev
32
+ ```
33
+
34
+ ## Features
35
+
36
+ - **Single Fund Simulation**: All capital in one fund
37
+ - **Single Fund Threshold**: Stop at target return (Treasury rate or NASDAQ average)
38
+ - **Multi Fund Simulation**: Diversified portfolio with multiple independent funds
39
+
40
+ ## Configuration
41
+
42
+ All simulation parameters can be configured through the web interface:
43
+ - Starting capital
44
+ - Number of simulations
45
+ - Probability thresholds
46
+ - Market selection skew
47
+ - Target returns (for threshold simulations)
48
+ - Number of funds (for multi-fund simulations)
49
+
static/index.html ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Safe Choices - Prediction Market Simulation</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js"></script>
10
+ </head>
11
+ <body>
12
+ <!-- Top Navigation Bar -->
13
+ <header class="top-bar">
14
+ <div class="nav-container">
15
+ <div class="logo-section">
16
+ <img src="/static/polymarket_logo.png" alt="Safe Choices" class="logo">
17
+ <h1 class="app-title">Safe Choices</h1>
18
+ <span class="subtitle">Prediction Market Simulation</span>
19
+ </div>
20
+ <div class="nav-info">
21
+ <span class="dataset-info">Dataset: Polymarket 2024-2025 Markets</span>
22
+ </div>
23
+ </div>
24
+ </header>
25
+
26
+ <!-- Main Container -->
27
+ <div class="main-container">
28
+ <!-- Simulation Controls -->
29
+ <section class="controls-section">
30
+ <div class="controls-header">
31
+ <h2>Simulation Configuration</h2>
32
+ <p>Configure your prediction market trading simulation parameters</p>
33
+ </div>
34
+
35
+ <div class="controls-grid">
36
+ <!-- Simulation Type Selection -->
37
+ <div class="control-group full-width">
38
+ <label class="control-label">Simulation Type</label>
39
+ <div class="simulation-tabs">
40
+ <button class="sim-tab active" data-sim="single" onclick="selectSimulation('single')">
41
+ <div class="tab-title">Single Fund</div>
42
+ <div class="tab-subtitle">All capital in one fund</div>
43
+ </button>
44
+ <button class="sim-tab" data-sim="threshold" onclick="selectSimulation('threshold')">
45
+ <div class="tab-title">Single Fund Threshold</div>
46
+ <div class="tab-subtitle">Stop at target return</div>
47
+ </button>
48
+ <button class="sim-tab" data-sim="multi" onclick="selectSimulation('multi')">
49
+ <div class="tab-title">Multi Fund</div>
50
+ <div class="tab-subtitle">Diversified portfolio</div>
51
+ </button>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Base Parameters -->
56
+ <div class="control-group">
57
+ <label class="control-label">Starting Capital ($)</label>
58
+ <input type="number" id="startingCapital" class="control-input" value="10000" min="1000" max="100000" step="1000">
59
+ <div class="control-subtitle">Total initial investment amount</div>
60
+ </div>
61
+
62
+ <div class="control-group">
63
+ <label class="control-label">Number of Simulations</label>
64
+ <input type="number" id="numSimulations" class="control-input" value="100" min="10" max="100" step="10">
65
+ <div class="control-subtitle">Monte Carlo simulation runs (max 100)</div>
66
+ </div>
67
+
68
+ <div class="control-group">
69
+ <label class="control-label">Start Date</label>
70
+ <input type="date" id="startDate" class="control-input" value="2025-01-01" min="2025-01-01">
71
+ <div class="control-subtitle">Simulation start date (2025+ only)</div>
72
+ </div>
73
+
74
+ <div class="control-group">
75
+ <label class="control-label">Max Duration (Days)</label>
76
+ <input type="number" id="maxDuration" class="control-input" value="365" min="30" max="730" step="30">
77
+ <div class="control-subtitle">Maximum simulation length</div>
78
+ </div>
79
+
80
+ <!-- Probability Thresholds -->
81
+ <div class="control-group">
82
+ <label class="control-label">Min Probability 7d (%)</label>
83
+ <input type="number" id="minProb7d" class="control-input" value="90" min="50" max="99" step="1">
84
+ <div class="control-subtitle">Minimum market probability 7 days before</div>
85
+ </div>
86
+
87
+ <div class="control-group">
88
+ <label class="control-label">Min Probability Current (%)</label>
89
+ <input type="number" id="minProbCurrent" class="control-input" value="90" min="50" max="99" step="1">
90
+ <div class="control-subtitle">Minimum probability at investment</div>
91
+ </div>
92
+
93
+ <div class="control-group">
94
+ <label class="control-label">Days Before Resolution</label>
95
+ <input type="number" id="daysBefore" class="control-input" value="1" min="1" max="7" step="1">
96
+ <div class="control-subtitle">When to invest before market closes</div>
97
+ </div>
98
+
99
+ <div class="control-group">
100
+ <label class="control-label">Market Selection Skew</label>
101
+ <input type="number" id="skewFactor" class="control-input" value="0.1" min="0.01" max="1.0" step="0.01">
102
+ <div class="control-subtitle">Higher values favor closer markets</div>
103
+ </div>
104
+
105
+ <!-- Threshold-specific controls -->
106
+ <div class="control-group threshold-only" style="display: none;">
107
+ <label class="control-label">Target Return (%)</label>
108
+ <select id="targetReturn" class="control-input">
109
+ <option value="4.14">4.14% (Treasury Rate)</option>
110
+ <option value="10.56">10.56% (NASDAQ Average)</option>
111
+ <option value="custom">Custom</option>
112
+ </select>
113
+ <input type="number" id="customTarget" class="control-input" style="display: none; margin-top: 8px;" min="1" max="50" step="0.1">
114
+ <div class="control-subtitle">Stop trading when target is reached</div>
115
+ </div>
116
+
117
+ <!-- Multi-fund specific controls -->
118
+ <div class="control-group multi-only" style="display: none;">
119
+ <label class="control-label">Number of Funds</label>
120
+ <input type="number" id="numFunds" class="control-input" value="5" min="2" max="10" step="1">
121
+ <div class="control-subtitle">Split capital into N independent funds (max 10)</div>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Run Button -->
126
+ <div class="run-section">
127
+ <button class="run-button" onclick="runSimulation()" id="runBtn">
128
+ <span class="run-text">Run Simulation</span>
129
+ <div class="run-spinner" style="display: none;"></div>
130
+ </button>
131
+ <div class="run-info">
132
+ <span id="estimatedTime">Estimated time: ~5-10 seconds</span>
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ <!-- Progress Bar -->
138
+ <section class="progress-section" style="display: none;">
139
+ <div class="progress-container">
140
+ <div class="progress-label">
141
+ <span id="progressText">Running simulation...</span>
142
+ <span id="progressPercent">0%</span>
143
+ </div>
144
+ <div class="progress-bar">
145
+ <div class="progress-fill" id="progressFill"></div>
146
+ </div>
147
+ </div>
148
+ </section>
149
+
150
+ <!-- Results Section -->
151
+ <section class="results-section" id="resultsSection" style="display: none;">
152
+ <div class="results-header">
153
+ <h2>Simulation Results</h2>
154
+ <button class="export-button" onclick="exportResults()" id="exportBtn">Export Data</button>
155
+ </div>
156
+
157
+ <!-- Summary Statistics -->
158
+ <div class="stats-grid">
159
+ <div class="stat-card">
160
+ <div class="stat-header">
161
+ <h3>Performance</h3>
162
+ </div>
163
+ <div class="stat-content">
164
+ <div class="stat-item">
165
+ <span class="stat-label">Average Return:</span>
166
+ <span class="stat-value" id="avgReturn">--</span>
167
+ </div>
168
+ <div class="stat-item">
169
+ <span class="stat-label">Median Return:</span>
170
+ <span class="stat-value" id="medianReturn">--</span>
171
+ </div>
172
+ <div class="stat-item">
173
+ <span class="stat-label">Success Rate:</span>
174
+ <span class="stat-value" id="successRate">--</span>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="stat-card">
180
+ <div class="stat-header">
181
+ <h3>Risk Metrics</h3>
182
+ </div>
183
+ <div class="stat-content">
184
+ <div class="stat-item">
185
+ <span class="stat-label">Bust Rate:</span>
186
+ <span class="stat-value" id="bustRate">--</span>
187
+ </div>
188
+ <div class="stat-item">
189
+ <span class="stat-label">Volatility:</span>
190
+ <span class="stat-value" id="volatility">--</span>
191
+ </div>
192
+ <div class="stat-item">
193
+ <span class="stat-label">Max Drawdown:</span>
194
+ <span class="stat-value" id="maxDrawdown">--</span>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="stat-card multi-stats" style="display: none;">
200
+ <div class="stat-header">
201
+ <h3>Portfolio Stats</h3>
202
+ </div>
203
+ <div class="stat-content">
204
+ <div class="stat-item">
205
+ <span class="stat-label">Avg Surviving Funds:</span>
206
+ <span class="stat-value" id="avgSurvivingFunds">--</span>
207
+ </div>
208
+ <div class="stat-item">
209
+ <span class="stat-label">Survivorship Rate:</span>
210
+ <span class="stat-value" id="survivorshipRate">--</span>
211
+ </div>
212
+ <div class="stat-item">
213
+ <span class="stat-label">Diversification Benefit:</span>
214
+ <span class="stat-value" id="diversificationBenefit">--</span>
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ <div class="stat-card threshold-stats" style="display: none;">
220
+ <div class="stat-header">
221
+ <h3>Target Achievement</h3>
222
+ </div>
223
+ <div class="stat-content">
224
+ <div class="stat-item">
225
+ <span class="stat-label">Target Reached:</span>
226
+ <span class="stat-value" id="targetReached">--</span>
227
+ </div>
228
+ <div class="stat-item">
229
+ <span class="stat-label">Avg Time to Target:</span>
230
+ <span class="stat-value" id="avgTimeToTarget">--</span>
231
+ </div>
232
+ <div class="stat-item">
233
+ <span class="stat-label">vs Never Stop:</span>
234
+ <span class="stat-value" id="vsNeverStop">--</span>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Charts -->
241
+ <div class="charts-grid">
242
+ <div class="chart-card">
243
+ <div class="chart-header">
244
+ <h3>Return Distribution</h3>
245
+ </div>
246
+ <div class="chart-container">
247
+ <canvas id="returnChart" width="400" height="200"></canvas>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="chart-card">
252
+ <div class="chart-header">
253
+ <h3>Capital Evolution</h3>
254
+ </div>
255
+ <div class="chart-container">
256
+ <canvas id="capitalChart" width="400" height="200"></canvas>
257
+ </div>
258
+ </div>
259
+
260
+ <div class="chart-card multi-chart" style="display: none;">
261
+ <div class="chart-header">
262
+ <h3>Fund Survivorship</h3>
263
+ </div>
264
+ <div class="chart-container">
265
+ <canvas id="survivorshipChart" width="400" height="200"></canvas>
266
+ </div>
267
+ </div>
268
+
269
+ <div class="chart-card">
270
+ <div class="chart-header">
271
+ <h3>Final Capital Distribution</h3>
272
+ </div>
273
+ <div class="chart-container">
274
+ <canvas id="distributionChart" width="400" height="200"></canvas>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </section>
279
+ </div>
280
+
281
+ <script src="/static/script.js"></script>
282
+ </body>
283
+ </html>
static/package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "safe-choices-frontend",
3
+ "version": "1.0.0",
4
+ "description": "Frontend for Safe Choices prediction market simulation",
5
+ "main": "index.html",
6
+ "scripts": {
7
+ "start": "npx http-server . -p 3000 -o",
8
+ "dev": "npx http-server . -p 3000 -o -c-1"
9
+ },
10
+ "keywords": [
11
+ "prediction-markets",
12
+ "simulation",
13
+ "polymarket"
14
+ ],
15
+ "author": "",
16
+ "license": "MIT"
17
+ }
18
+
static/polymarket_logo.png ADDED
static/script.js ADDED
@@ -0,0 +1,798 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global variables
2
+ let currentSimType = 'single';
3
+ let simulationResults = null;
4
+ let charts = {};
5
+
6
+ // Initialize the application
7
+ document.addEventListener('DOMContentLoaded', function() {
8
+ initializeDefaults();
9
+ setupEventListeners();
10
+ updateEstimatedTime();
11
+ });
12
+
13
+ // Initialize default values and constraints
14
+ function initializeDefaults() {
15
+ // Set minimum start date and default to 2025-01-01
16
+ const startDateInput = document.getElementById('startDate');
17
+ const defaultDate = new Date('2025-01-01');
18
+
19
+ startDateInput.value = defaultDate.toISOString().split('T')[0];
20
+ startDateInput.min = '2025-01-01';
21
+
22
+ // Initialize target return dropdown handler
23
+ document.getElementById('targetReturn').addEventListener('change', function() {
24
+ const customInput = document.getElementById('customTarget');
25
+ if (this.value === 'custom') {
26
+ customInput.style.display = 'block';
27
+ customInput.value = '12.0';
28
+ } else {
29
+ customInput.style.display = 'none';
30
+ }
31
+ });
32
+ }
33
+
34
+ // Setup event listeners
35
+ function setupEventListeners() {
36
+ // Input validation
37
+ document.getElementById('numSimulations').addEventListener('input', function() {
38
+ this.value = Math.min(Math.max(parseInt(this.value) || 10, 10), 100);
39
+ updateEstimatedTime();
40
+ });
41
+
42
+ document.getElementById('numFunds').addEventListener('input', function() {
43
+ this.value = Math.min(Math.max(parseInt(this.value) || 2, 2), 10);
44
+ });
45
+
46
+ // Update estimated time when parameters change
47
+ const timeInputs = ['numSimulations', 'maxDuration', 'numFunds'];
48
+ timeInputs.forEach(id => {
49
+ document.getElementById(id).addEventListener('input', updateEstimatedTime);
50
+ });
51
+
52
+ // Real-time validation
53
+ const inputs = document.querySelectorAll('.control-input');
54
+ inputs.forEach(input => {
55
+ input.addEventListener('input', validateInputs);
56
+ });
57
+ }
58
+
59
+ // Select simulation type
60
+ function selectSimulation(type) {
61
+ currentSimType = type;
62
+
63
+ // Update tab appearance
64
+ document.querySelectorAll('.sim-tab').forEach(tab => {
65
+ tab.classList.remove('active');
66
+ });
67
+ document.querySelector(`[data-sim="${type}"]`).classList.add('active');
68
+
69
+ // Show/hide conditional controls
70
+ document.querySelectorAll('.threshold-only').forEach(el => {
71
+ el.style.display = type === 'threshold' ? 'block' : 'none';
72
+ });
73
+
74
+ document.querySelectorAll('.multi-only').forEach(el => {
75
+ el.style.display = type === 'multi' ? 'block' : 'none';
76
+ });
77
+
78
+ updateEstimatedTime();
79
+ }
80
+
81
+ // Update estimated execution time
82
+ function updateEstimatedTime() {
83
+ const numSims = parseInt(document.getElementById('numSimulations').value) || 100;
84
+ const maxDuration = parseInt(document.getElementById('maxDuration').value) || 365;
85
+ const numFunds = currentSimType === 'multi' ? (parseInt(document.getElementById('numFunds').value) || 5) : 1;
86
+
87
+ // Rough estimation based on complexity
88
+ let baseTime = numSims * 0.05; // ~50ms per simulation
89
+ baseTime *= (maxDuration / 365); // Scale by duration
90
+ if (currentSimType === 'multi') {
91
+ baseTime *= Math.sqrt(numFunds); // Multi-fund overhead
92
+ }
93
+
94
+ const estimatedSeconds = Math.max(2, Math.ceil(baseTime));
95
+
96
+ let timeText = '';
97
+ if (estimatedSeconds < 60) {
98
+ timeText = `~${estimatedSeconds} seconds`;
99
+ } else {
100
+ const minutes = Math.ceil(estimatedSeconds / 60);
101
+ timeText = `~${minutes} minute${minutes > 1 ? 's' : ''}`;
102
+ }
103
+
104
+ document.getElementById('estimatedTime').textContent = `Estimated time: ${timeText}`;
105
+ }
106
+
107
+ // Validate all inputs
108
+ function validateInputs() {
109
+ const errors = [];
110
+
111
+ // Check required fields
112
+ const startingCapital = parseFloat(document.getElementById('startingCapital').value);
113
+ if (!startingCapital || startingCapital < 1000) {
114
+ errors.push('Starting capital must be at least $1,000');
115
+ }
116
+
117
+ const startDate = new Date(document.getElementById('startDate').value);
118
+ const minDate = new Date('2025-01-01');
119
+ if (startDate < minDate) {
120
+ errors.push('Start date must be 2025-01-01 or later');
121
+ }
122
+
123
+ const minProb7d = parseFloat(document.getElementById('minProb7d').value);
124
+ const minProbCurrent = parseFloat(document.getElementById('minProbCurrent').value);
125
+ if (minProb7d < 50 || minProb7d > 99) {
126
+ errors.push('7-day probability must be between 50% and 99%');
127
+ }
128
+ if (minProbCurrent < 50 || minProbCurrent > 99) {
129
+ errors.push('Current probability must be between 50% and 99%');
130
+ }
131
+
132
+ // Update run button state
133
+ const runBtn = document.getElementById('runBtn');
134
+ runBtn.disabled = errors.length > 0;
135
+
136
+ return errors.length === 0;
137
+ }
138
+
139
+ // Main simulation runner
140
+ async function runSimulation() {
141
+ if (!validateInputs()) return;
142
+
143
+ // Get parameters
144
+ const params = getSimulationParameters();
145
+
146
+ // Update UI for running state
147
+ showProgress();
148
+ disableControls();
149
+
150
+ try {
151
+ // Call the backend API
152
+ const results = await callSimulationAPI(params);
153
+
154
+ // Store results and display
155
+ simulationResults = results;
156
+ displayResults(results);
157
+
158
+ } catch (error) {
159
+ console.error('Simulation error:', error);
160
+ alert('An error occurred during simulation: ' + (error.message || 'Unknown error'));
161
+ } finally {
162
+ hideProgress();
163
+ enableControls();
164
+ }
165
+ }
166
+
167
+ // Call the backend API for real simulations
168
+ async function callSimulationAPI(params) {
169
+ const progressFill = document.getElementById('progressFill');
170
+ const progressPercent = document.getElementById('progressPercent');
171
+ const progressText = document.getElementById('progressText');
172
+
173
+ progressText.textContent = 'Sending request to backend...';
174
+ progressFill.style.width = '10%';
175
+ progressPercent.textContent = '10%';
176
+
177
+ // Prepare request body
178
+ const requestBody = {
179
+ simType: params.simType,
180
+ startingCapital: params.startingCapital,
181
+ numSimulations: params.numSimulations,
182
+ startDate: params.startDate,
183
+ maxDuration: params.maxDuration,
184
+ minProb7d: params.minProb7d,
185
+ minProbCurrent: params.minProbCurrent,
186
+ daysBefore: params.daysBefore,
187
+ skewFactor: params.skewFactor
188
+ };
189
+
190
+ if (params.simType === 'threshold' && params.targetReturn !== undefined) {
191
+ requestBody.targetReturn = params.targetReturn;
192
+ }
193
+
194
+ if (params.simType === 'multi' && params.numFunds !== undefined) {
195
+ requestBody.numFunds = params.numFunds;
196
+ }
197
+
198
+ // Make API call
199
+ const response = await fetch('/api/simulate', {
200
+ method: 'POST',
201
+ headers: {
202
+ 'Content-Type': 'application/json',
203
+ },
204
+ body: JSON.stringify(requestBody)
205
+ });
206
+
207
+ if (!response.ok) {
208
+ const errorData = await response.json().catch(() => ({ detail: response.statusText }));
209
+ throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
210
+ }
211
+
212
+ progressText.textContent = 'Processing results...';
213
+ progressFill.style.width = '90%';
214
+ progressPercent.textContent = '90%';
215
+
216
+ const results = await response.json();
217
+
218
+ progressFill.style.width = '100%';
219
+ progressPercent.textContent = '100%';
220
+ progressText.textContent = 'Complete!';
221
+
222
+ return results;
223
+ }
224
+
225
+ // Get simulation parameters from UI
226
+ function getSimulationParameters() {
227
+ const params = {
228
+ simType: currentSimType,
229
+ startingCapital: parseFloat(document.getElementById('startingCapital').value),
230
+ numSimulations: parseInt(document.getElementById('numSimulations').value),
231
+ startDate: document.getElementById('startDate').value,
232
+ maxDuration: parseInt(document.getElementById('maxDuration').value),
233
+ minProb7d: parseFloat(document.getElementById('minProb7d').value) / 100,
234
+ minProbCurrent: parseFloat(document.getElementById('minProbCurrent').value) / 100,
235
+ daysBefore: parseInt(document.getElementById('daysBefore').value),
236
+ skewFactor: parseFloat(document.getElementById('skewFactor').value)
237
+ };
238
+
239
+ // Type-specific parameters
240
+ if (currentSimType === 'threshold') {
241
+ const targetSelect = document.getElementById('targetReturn').value;
242
+ if (targetSelect === 'custom') {
243
+ params.targetReturn = parseFloat(document.getElementById('customTarget').value) / 100;
244
+ } else {
245
+ params.targetReturn = parseFloat(targetSelect) / 100;
246
+ }
247
+ }
248
+
249
+ if (currentSimType === 'multi') {
250
+ params.numFunds = parseInt(document.getElementById('numFunds').value);
251
+ }
252
+
253
+ return params;
254
+ }
255
+
256
+ // Legacy function - kept for reference but not used when API is available
257
+ async function simulateComputation(params) {
258
+ // This function is replaced by callSimulationAPI
259
+ // Kept for fallback if needed
260
+ return await callSimulationAPI(params);
261
+ }
262
+
263
+ // Generate a realistic simulation run
264
+ function generateSimulationRun(params, seed) {
265
+ const rng = new SimpleRNG(seed + 12345);
266
+
267
+ const run = {
268
+ finalCapital: 0,
269
+ totalReturn: 0,
270
+ numTrades: 0,
271
+ wentBust: false,
272
+ reachedTarget: false,
273
+ simulationDays: 0
274
+ };
275
+
276
+ if (params.simType === 'multi') {
277
+ run.survivingFunds = 0;
278
+ run.fundResults = [];
279
+
280
+ for (let f = 0; f < params.numFunds; f++) {
281
+ const fundRun = generateSingleFundRun(params, rng.next());
282
+ run.fundResults.push(fundRun);
283
+ if (!fundRun.wentBust) run.survivingFunds++;
284
+ }
285
+
286
+ run.finalCapital = run.fundResults.reduce((sum, fund) => sum + fund.finalCapital, 0);
287
+ run.totalReturn = (run.finalCapital - params.startingCapital) / params.startingCapital;
288
+ run.numTrades = run.fundResults.reduce((sum, fund) => sum + fund.numTrades, 0);
289
+ run.wentBust = run.survivingFunds === 0;
290
+ } else {
291
+ const singleRun = generateSingleFundRun(params, seed);
292
+ Object.assign(run, singleRun);
293
+ }
294
+
295
+ return run;
296
+ }
297
+
298
+ // Generate a single fund run with realistic market behavior
299
+ function generateSingleFundRun(params, seed) {
300
+ const rng = new SimpleRNG(seed);
301
+ let capital = params.simType === 'multi' ? params.startingCapital / params.numFunds : params.startingCapital;
302
+
303
+ const run = {
304
+ finalCapital: capital,
305
+ totalReturn: 0,
306
+ numTrades: 0,
307
+ wentBust: false,
308
+ reachedTarget: false,
309
+ simulationDays: 0,
310
+ capitalHistory: [capital]
311
+ };
312
+
313
+ const maxTrades = Math.floor(params.maxDuration / 7) + rng.nextInt(5, 20);
314
+ let day = 0;
315
+
316
+ for (let trade = 0; trade < maxTrades && capital > 0 && day < params.maxDuration; trade++) {
317
+ // Check target return for threshold simulations
318
+ if (params.simType === 'threshold' && params.targetReturn) {
319
+ const currentReturn = (capital - (params.startingCapital / (params.numFunds || 1))) / (params.startingCapital / (params.numFunds || 1));
320
+ if (currentReturn >= params.targetReturn) {
321
+ run.reachedTarget = true;
322
+ break;
323
+ }
324
+ }
325
+
326
+ // Market probability based on thresholds (realistic distribution)
327
+ const marketProb = Math.max(params.minProbCurrent,
328
+ params.minProbCurrent + rng.nextGaussian() * 0.05);
329
+
330
+ // Win probability (slightly higher than market prob to simulate edge)
331
+ const winProb = Math.min(0.99, marketProb + 0.01 + rng.nextGaussian() * 0.02);
332
+
333
+ const won = rng.nextFloat() < winProb;
334
+
335
+ if (won) {
336
+ capital = capital / marketProb; // Return = 1/probability
337
+ } else {
338
+ capital = 0; // Total loss
339
+ run.wentBust = true;
340
+ break;
341
+ }
342
+
343
+ run.numTrades++;
344
+ run.capitalHistory.push(capital);
345
+
346
+ // Advance time (realistic trading frequency)
347
+ day += rng.nextInt(1, 14);
348
+
349
+ // Ending factor check
350
+ const endingFactor = 0.05 + (day * 0.001);
351
+ if (rng.nextFloat() < endingFactor) break;
352
+ }
353
+
354
+ run.finalCapital = capital;
355
+ run.simulationDays = day;
356
+ const initialCapital = params.simType === 'multi' ? params.startingCapital / params.numFunds : params.startingCapital;
357
+ run.totalReturn = (capital - initialCapital) / initialCapital;
358
+
359
+ return run;
360
+ }
361
+
362
+ // Calculate summary statistics
363
+ function calculateSummaryStats(runs, params) {
364
+ const returns = runs.map(r => r.totalReturn);
365
+ const capitals = runs.map(r => r.finalCapital);
366
+ const trades = runs.map(r => r.numTrades);
367
+
368
+ const summary = {
369
+ avgReturn: mean(returns),
370
+ medianReturn: median(returns),
371
+ returnVolatility: standardDeviation(returns),
372
+ avgFinalCapital: mean(capitals),
373
+ medianFinalCapital: median(capitals),
374
+ bustRate: runs.filter(r => r.wentBust).length / runs.length,
375
+ positiveReturnRate: returns.filter(r => r > 0).length / returns.length,
376
+ avgTrades: mean(trades),
377
+
378
+ // Percentiles
379
+ return5th: percentile(returns, 5),
380
+ return95th: percentile(returns, 95),
381
+
382
+ // Max drawdown approximation
383
+ maxDrawdown: Math.abs(Math.min(...returns))
384
+ };
385
+
386
+ // Type-specific stats
387
+ if (params.simType === 'threshold' && params.targetReturn) {
388
+ const reachedTarget = runs.filter(r => r.reachedTarget).length;
389
+ summary.targetReachedRate = reachedTarget / runs.length;
390
+ summary.avgTimeToTarget = mean(runs.filter(r => r.reachedTarget).map(r => r.simulationDays)) || 0;
391
+ }
392
+
393
+ if (params.simType === 'multi') {
394
+ const survivingFunds = runs.map(r => r.survivingFunds);
395
+ summary.avgSurvivingFunds = mean(survivingFunds);
396
+ summary.survivorshipRate = mean(survivingFunds) / params.numFunds;
397
+ summary.portfolioBustRate = runs.filter(r => r.survivingFunds === 0).length / runs.length;
398
+ }
399
+
400
+ return summary;
401
+ }
402
+
403
+ // Display results in UI
404
+ function displayResults(results) {
405
+ const { summary, parameters } = results;
406
+
407
+ // Show results section
408
+ document.getElementById('resultsSection').style.display = 'block';
409
+ document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });
410
+
411
+ // Update basic statistics
412
+ document.getElementById('avgReturn').textContent = formatPercentage(summary.avgReturn);
413
+ document.getElementById('avgReturn').className = `stat-value ${getReturnClass(summary.avgReturn)}`;
414
+
415
+ document.getElementById('medianReturn').textContent = formatPercentage(summary.medianReturn);
416
+ document.getElementById('medianReturn').className = `stat-value ${getReturnClass(summary.medianReturn)}`;
417
+
418
+ document.getElementById('successRate').textContent = formatPercentage(summary.positiveReturnRate);
419
+
420
+ document.getElementById('bustRate').textContent = formatPercentage(summary.bustRate);
421
+ document.getElementById('bustRate').className = `stat-value ${summary.bustRate > 0.1 ? 'negative' : 'positive'}`;
422
+
423
+ document.getElementById('volatility').textContent = formatPercentage(summary.returnVolatility);
424
+ document.getElementById('maxDrawdown').textContent = formatPercentage(summary.maxDrawdown);
425
+
426
+ // Type-specific statistics
427
+ if (parameters.simType === 'multi') {
428
+ document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
429
+ el.style.display = 'block';
430
+ });
431
+
432
+ document.getElementById('avgSurvivingFunds').textContent =
433
+ `${summary.avgSurvivingFunds.toFixed(1)} / ${parameters.numFunds}`;
434
+ document.getElementById('survivorshipRate').textContent = formatPercentage(summary.survivorshipRate);
435
+
436
+ // Simple diversification benefit calculation
437
+ const diversificationBenefit = summary.returnVolatility < 0.3 ? 'Positive' : 'Limited';
438
+ document.getElementById('diversificationBenefit').textContent = diversificationBenefit;
439
+ } else {
440
+ document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
441
+ el.style.display = 'none';
442
+ });
443
+ }
444
+
445
+ if (parameters.simType === 'threshold') {
446
+ document.querySelectorAll('.threshold-stats').forEach(el => {
447
+ el.style.display = 'block';
448
+ });
449
+
450
+ document.getElementById('targetReached').textContent = formatPercentage(summary.targetReachedRate || 0);
451
+ document.getElementById('avgTimeToTarget').textContent =
452
+ summary.avgTimeToTarget ? `${Math.round(summary.avgTimeToTarget)} days` : 'N/A';
453
+ document.getElementById('vsNeverStop').textContent =
454
+ summary.targetReachedRate > 0.5 ? 'Better' : 'Similar';
455
+ } else {
456
+ document.querySelectorAll('.threshold-stats').forEach(el => {
457
+ el.style.display = 'none';
458
+ });
459
+ }
460
+
461
+ // Generate charts
462
+ generateCharts(results);
463
+ }
464
+
465
+ // Generate all charts
466
+ function generateCharts(results) {
467
+ const { runs, summary, parameters } = results;
468
+
469
+ // Destroy existing charts
470
+ Object.values(charts).forEach(chart => {
471
+ if (chart) chart.destroy();
472
+ });
473
+ charts = {};
474
+
475
+ // Return distribution histogram
476
+ charts.return = createReturnDistributionChart(runs);
477
+
478
+ // Capital evolution (sample of runs)
479
+ charts.capital = createCapitalEvolutionChart(runs.slice(0, 10));
480
+
481
+ // Final capital distribution
482
+ charts.distribution = createCapitalDistributionChart(runs);
483
+
484
+ // Multi-fund specific chart
485
+ if (parameters.simType === 'multi') {
486
+ charts.survivorship = createSurvivorshipChart(runs, parameters.numFunds);
487
+ }
488
+ }
489
+
490
+ // Create return distribution chart
491
+ function createReturnDistributionChart(runs) {
492
+ const ctx = document.getElementById('returnChart').getContext('2d');
493
+ const returns = runs.map(r => r.totalReturn * 100);
494
+
495
+ const histogram = createHistogram(returns, 20);
496
+
497
+ return new Chart(ctx, {
498
+ type: 'bar',
499
+ data: {
500
+ labels: histogram.labels,
501
+ datasets: [{
502
+ label: 'Frequency',
503
+ data: histogram.data,
504
+ backgroundColor: 'rgba(59, 130, 246, 0.7)',
505
+ borderColor: 'rgba(59, 130, 246, 1)',
506
+ borderWidth: 1
507
+ }]
508
+ },
509
+ options: {
510
+ responsive: true,
511
+ maintainAspectRatio: false,
512
+ plugins: {
513
+ legend: { display: false }
514
+ },
515
+ scales: {
516
+ x: {
517
+ title: {
518
+ display: true,
519
+ text: 'Return (%)'
520
+ }
521
+ },
522
+ y: {
523
+ title: {
524
+ display: true,
525
+ text: 'Frequency'
526
+ }
527
+ }
528
+ }
529
+ }
530
+ });
531
+ }
532
+
533
+ // Create capital evolution chart
534
+ function createCapitalEvolutionChart(sampleRuns) {
535
+ const ctx = document.getElementById('capitalChart').getContext('2d');
536
+
537
+ const datasets = sampleRuns.slice(0, 5).map((run, i) => ({
538
+ label: `Run ${i + 1}`,
539
+ data: run.capitalHistory || [run.finalCapital],
540
+ borderColor: `hsla(${i * 60}, 70%, 50%, 0.8)`,
541
+ backgroundColor: `hsla(${i * 60}, 70%, 50%, 0.1)`,
542
+ borderWidth: 2,
543
+ fill: false
544
+ }));
545
+
546
+ return new Chart(ctx, {
547
+ type: 'line',
548
+ data: { datasets },
549
+ options: {
550
+ responsive: true,
551
+ maintainAspectRatio: false,
552
+ plugins: {
553
+ legend: { display: true }
554
+ },
555
+ scales: {
556
+ x: {
557
+ title: {
558
+ display: true,
559
+ text: 'Trade Number'
560
+ }
561
+ },
562
+ y: {
563
+ title: {
564
+ display: true,
565
+ text: 'Capital ($)'
566
+ }
567
+ }
568
+ }
569
+ }
570
+ });
571
+ }
572
+
573
+ // Create capital distribution chart
574
+ function createCapitalDistributionChart(runs) {
575
+ const ctx = document.getElementById('distributionChart').getContext('2d');
576
+ const capitals = runs.map(r => r.finalCapital);
577
+
578
+ const histogram = createHistogram(capitals, 15);
579
+
580
+ return new Chart(ctx, {
581
+ type: 'bar',
582
+ data: {
583
+ labels: histogram.labels,
584
+ datasets: [{
585
+ label: 'Frequency',
586
+ data: histogram.data,
587
+ backgroundColor: 'rgba(16, 185, 129, 0.7)',
588
+ borderColor: 'rgba(16, 185, 129, 1)',
589
+ borderWidth: 1
590
+ }]
591
+ },
592
+ options: {
593
+ responsive: true,
594
+ maintainAspectRatio: false,
595
+ plugins: {
596
+ legend: { display: false }
597
+ },
598
+ scales: {
599
+ x: {
600
+ title: {
601
+ display: true,
602
+ text: 'Final Capital ($)'
603
+ }
604
+ },
605
+ y: {
606
+ title: {
607
+ display: true,
608
+ text: 'Frequency'
609
+ }
610
+ }
611
+ }
612
+ }
613
+ });
614
+ }
615
+
616
+ // Create survivorship chart for multi-fund
617
+ function createSurvivorshipChart(runs, numFunds) {
618
+ const ctx = document.getElementById('survivorshipChart').getContext('2d');
619
+ const survivingCounts = runs.map(r => r.survivingFunds);
620
+
621
+ const histogram = {};
622
+ for (let i = 0; i <= numFunds; i++) {
623
+ histogram[i] = survivingCounts.filter(c => c === i).length;
624
+ }
625
+
626
+ return new Chart(ctx, {
627
+ type: 'bar',
628
+ data: {
629
+ labels: Object.keys(histogram),
630
+ datasets: [{
631
+ label: 'Frequency',
632
+ data: Object.values(histogram),
633
+ backgroundColor: 'rgba(245, 158, 11, 0.7)',
634
+ borderColor: 'rgba(245, 158, 11, 1)',
635
+ borderWidth: 1
636
+ }]
637
+ },
638
+ options: {
639
+ responsive: true,
640
+ maintainAspectRatio: false,
641
+ plugins: {
642
+ legend: { display: false }
643
+ },
644
+ scales: {
645
+ x: {
646
+ title: {
647
+ display: true,
648
+ text: 'Surviving Funds'
649
+ }
650
+ },
651
+ y: {
652
+ title: {
653
+ display: true,
654
+ text: 'Frequency'
655
+ }
656
+ }
657
+ }
658
+ }
659
+ });
660
+ }
661
+
662
+ // Export results
663
+ function exportResults() {
664
+ if (!simulationResults) return;
665
+
666
+ const data = {
667
+ timestamp: new Date().toISOString(),
668
+ parameters: simulationResults.parameters,
669
+ summary: simulationResults.summary,
670
+ runs: simulationResults.runs
671
+ };
672
+
673
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
674
+ const url = URL.createObjectURL(blob);
675
+
676
+ const a = document.createElement('a');
677
+ a.href = url;
678
+ a.download = `safe_choices_simulation_${Date.now()}.json`;
679
+ document.body.appendChild(a);
680
+ a.click();
681
+ document.body.removeChild(a);
682
+ URL.revokeObjectURL(url);
683
+ }
684
+
685
+ // UI control functions
686
+ function showProgress() {
687
+ document.querySelector('.progress-section').style.display = 'block';
688
+ document.getElementById('progressFill').style.width = '0%';
689
+ }
690
+
691
+ function hideProgress() {
692
+ setTimeout(() => {
693
+ document.querySelector('.progress-section').style.display = 'none';
694
+ }, 500);
695
+ }
696
+
697
+ function disableControls() {
698
+ const runBtn = document.getElementById('runBtn');
699
+ runBtn.disabled = true;
700
+ document.querySelector('.run-text').textContent = 'Running...';
701
+ document.querySelector('.run-spinner').style.display = 'inline-block';
702
+ }
703
+
704
+ function enableControls() {
705
+ const runBtn = document.getElementById('runBtn');
706
+ runBtn.disabled = false;
707
+ document.querySelector('.run-text').textContent = 'Run Simulation';
708
+ document.querySelector('.run-spinner').style.display = 'none';
709
+ }
710
+
711
+ // Utility functions
712
+ function sleep(ms) {
713
+ return new Promise(resolve => setTimeout(resolve, ms));
714
+ }
715
+
716
+ function formatPercentage(value) {
717
+ return `${(value * 100).toFixed(1)}%`;
718
+ }
719
+
720
+ function getReturnClass(value) {
721
+ if (value > 0.02) return 'positive';
722
+ if (value < -0.02) return 'negative';
723
+ return 'neutral';
724
+ }
725
+
726
+ function mean(arr) {
727
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
728
+ }
729
+
730
+ function median(arr) {
731
+ const sorted = [...arr].sort((a, b) => a - b);
732
+ const mid = Math.floor(sorted.length / 2);
733
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
734
+ }
735
+
736
+ function standardDeviation(arr) {
737
+ const avg = mean(arr);
738
+ const squareDiffs = arr.map(value => Math.pow(value - avg, 2));
739
+ return Math.sqrt(mean(squareDiffs));
740
+ }
741
+
742
+ function percentile(arr, p) {
743
+ const sorted = [...arr].sort((a, b) => a - b);
744
+ const index = (p / 100) * (sorted.length - 1);
745
+ const lower = Math.floor(index);
746
+ const upper = Math.ceil(index);
747
+ const weight = index % 1;
748
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
749
+ }
750
+
751
+ function createHistogram(data, bins) {
752
+ const min = Math.min(...data);
753
+ const max = Math.max(...data);
754
+ const binWidth = (max - min) / bins;
755
+
756
+ const histogram = new Array(bins).fill(0);
757
+ const labels = [];
758
+
759
+ for (let i = 0; i < bins; i++) {
760
+ const binStart = min + i * binWidth;
761
+ const binEnd = min + (i + 1) * binWidth;
762
+ labels.push(`${binStart.toFixed(1)}`);
763
+
764
+ for (const value of data) {
765
+ if (value >= binStart && (value < binEnd || i === bins - 1)) {
766
+ histogram[i]++;
767
+ }
768
+ }
769
+ }
770
+
771
+ return { labels, data: histogram };
772
+ }
773
+
774
+ // Simple RNG for reproducible results
775
+ class SimpleRNG {
776
+ constructor(seed) {
777
+ this.seed = seed % 2147483647;
778
+ if (this.seed <= 0) this.seed += 2147483646;
779
+ }
780
+
781
+ next() {
782
+ return this.seed = this.seed * 16807 % 2147483647;
783
+ }
784
+
785
+ nextFloat() {
786
+ return (this.next() - 1) / 2147483646;
787
+ }
788
+
789
+ nextGaussian() {
790
+ const u = 0.5 - this.nextFloat();
791
+ const v = this.nextFloat();
792
+ return Math.sqrt(-2.0 * Math.log(v)) * Math.cos(2.0 * Math.PI * u);
793
+ }
794
+
795
+ nextInt(min, max) {
796
+ return Math.floor(this.nextFloat() * (max - min + 1)) + min;
797
+ }
798
+ }
static/styles.css ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CSS Variables for Polymarket-inspired design */
2
+ :root {
3
+ --polymarket-blue: #3B82F6;
4
+ --polymarket-blue-dark: #2563EB;
5
+ --polymarket-blue-light: #DBEAFE;
6
+ --light-gray: #F8FAFC;
7
+ --medium-gray: #E2E8F0;
8
+ --dark-gray: #64748B;
9
+ --text-primary: #1E293B;
10
+ --text-secondary: #475569;
11
+ --white: #FFFFFF;
12
+ --success: #10B981;
13
+ --warning: #F59E0B;
14
+ --error: #EF4444;
15
+ --shadow-sm: 0 1px 1px 0 rgba(0, 0, 0, 0.03);
16
+ --shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
17
+ --shadow-lg: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
18
+ }
19
+
20
+ /* Reset and base styles */
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
29
+ background-color: var(--light-gray);
30
+ color: var(--text-primary);
31
+ line-height: 1.5;
32
+ min-height: 100vh;
33
+ }
34
+
35
+ /* Top Navigation Bar */
36
+ .top-bar {
37
+ background-color: var(--white);
38
+ border-bottom: 1px solid var(--medium-gray);
39
+ box-shadow: var(--shadow-sm);
40
+ position: sticky;
41
+ top: 0;
42
+ z-index: 100;
43
+ }
44
+
45
+ .nav-container {
46
+ max-width: 1200px;
47
+ margin: 0 auto;
48
+ padding: 0 32px;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ height: 72px;
53
+ }
54
+
55
+ .logo-section {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 12px;
59
+ }
60
+
61
+ .logo {
62
+ height: 32px;
63
+ width: auto;
64
+ }
65
+
66
+ .app-title {
67
+ font-size: 20px;
68
+ font-weight: 600;
69
+ color: var(--polymarket-blue);
70
+ }
71
+
72
+ .subtitle {
73
+ font-size: 14px;
74
+ color: var(--text-secondary);
75
+ font-weight: 400;
76
+ }
77
+
78
+ .nav-info {
79
+ display: flex;
80
+ align-items: center;
81
+ }
82
+
83
+ .dataset-info {
84
+ font-size: 14px;
85
+ color: var(--text-secondary);
86
+ background: var(--light-gray);
87
+ padding: 6px 12px;
88
+ border-radius: 20px;
89
+ }
90
+
91
+ /* Main Container */
92
+ .main-container {
93
+ max-width: 1200px;
94
+ margin: 0 auto;
95
+ padding: 32px;
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 32px;
99
+ }
100
+
101
+ /* Controls Section */
102
+ .controls-section {
103
+ background: var(--white);
104
+ border-radius: 12px;
105
+ padding: 32px;
106
+ box-shadow: var(--shadow-md);
107
+ border: 1px solid var(--medium-gray);
108
+ }
109
+
110
+ .controls-header {
111
+ margin-bottom: 32px;
112
+ }
113
+
114
+ .controls-header h2 {
115
+ font-size: 24px;
116
+ font-weight: 600;
117
+ margin-bottom: 8px;
118
+ color: var(--text-primary);
119
+ }
120
+
121
+ .controls-header p {
122
+ font-size: 16px;
123
+ color: var(--text-secondary);
124
+ }
125
+
126
+ .controls-grid {
127
+ display: grid;
128
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
129
+ gap: 24px;
130
+ margin-bottom: 32px;
131
+ }
132
+
133
+ .control-group {
134
+ display: flex;
135
+ flex-direction: column;
136
+ }
137
+
138
+ .control-group.full-width {
139
+ grid-column: 1 / -1;
140
+ }
141
+
142
+ .control-label {
143
+ font-size: 14px;
144
+ font-weight: 500;
145
+ color: var(--text-primary);
146
+ margin-bottom: 10px;
147
+ }
148
+
149
+ .control-input {
150
+ padding: 12px 16px;
151
+ border: 1px solid var(--medium-gray);
152
+ border-radius: 8px;
153
+ font-size: 14px;
154
+ background: var(--white);
155
+ transition: all 0.2s;
156
+ outline: none;
157
+ }
158
+
159
+ .control-input:focus {
160
+ border-color: var(--polymarket-blue);
161
+ box-shadow: 0 0 0 3px var(--polymarket-blue-light);
162
+ }
163
+
164
+ .control-subtitle {
165
+ font-size: 12px;
166
+ color: var(--text-secondary);
167
+ margin-top: 6px;
168
+ }
169
+
170
+ /* Simulation Tabs */
171
+ .simulation-tabs {
172
+ display: flex;
173
+ gap: 16px;
174
+ margin-top: 12px;
175
+ }
176
+
177
+ .sim-tab {
178
+ flex: 1;
179
+ padding: 20px;
180
+ border: 1px solid var(--medium-gray);
181
+ border-radius: 8px;
182
+ background: var(--white);
183
+ cursor: pointer;
184
+ transition: all 0.2s;
185
+ text-align: left;
186
+ }
187
+
188
+ .sim-tab:hover {
189
+ border-color: var(--polymarket-blue-light);
190
+ background: var(--polymarket-blue-light);
191
+ }
192
+
193
+ .sim-tab.active {
194
+ border-color: var(--polymarket-blue);
195
+ background: var(--polymarket-blue-light);
196
+ }
197
+
198
+ .tab-title {
199
+ font-size: 16px;
200
+ font-weight: 600;
201
+ color: var(--text-primary);
202
+ margin-bottom: 4px;
203
+ }
204
+
205
+ .tab-subtitle {
206
+ font-size: 14px;
207
+ color: var(--text-secondary);
208
+ }
209
+
210
+ /* Run Section */
211
+ .run-section {
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ gap: 16px;
216
+ padding-top: 32px;
217
+ border-top: 1px solid var(--medium-gray);
218
+ margin-top: 8px;
219
+ }
220
+
221
+ .run-button {
222
+ background: var(--polymarket-blue);
223
+ color: var(--white);
224
+ border: none;
225
+ padding: 14px 32px;
226
+ border-radius: 8px;
227
+ font-size: 16px;
228
+ font-weight: 600;
229
+ cursor: pointer;
230
+ transition: all 0.2s;
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ }
235
+
236
+ .run-button:hover:not(:disabled) {
237
+ background: var(--polymarket-blue-dark);
238
+ }
239
+
240
+ .run-button:disabled {
241
+ background: var(--dark-gray);
242
+ cursor: not-allowed;
243
+ }
244
+
245
+ .run-spinner {
246
+ width: 20px;
247
+ height: 20px;
248
+ border: 2px solid transparent;
249
+ border-top: 2px solid var(--white);
250
+ border-radius: 50%;
251
+ animation: spin 1s linear infinite;
252
+ }
253
+
254
+ @keyframes spin {
255
+ 0% { transform: rotate(0deg); }
256
+ 100% { transform: rotate(360deg); }
257
+ }
258
+
259
+ .run-info {
260
+ font-size: 14px;
261
+ color: var(--text-secondary);
262
+ }
263
+
264
+ /* Progress Section */
265
+ .progress-section {
266
+ background: var(--white);
267
+ border-radius: 12px;
268
+ padding: 32px;
269
+ box-shadow: var(--shadow-md);
270
+ border: 1px solid var(--medium-gray);
271
+ }
272
+
273
+ .progress-container {
274
+ max-width: 600px;
275
+ margin: 0 auto;
276
+ }
277
+
278
+ .progress-label {
279
+ display: flex;
280
+ justify-content: space-between;
281
+ align-items: center;
282
+ margin-bottom: 12px;
283
+ }
284
+
285
+ .progress-bar {
286
+ width: 100%;
287
+ height: 8px;
288
+ background: var(--medium-gray);
289
+ border-radius: 4px;
290
+ overflow: hidden;
291
+ }
292
+
293
+ .progress-fill {
294
+ height: 100%;
295
+ background: var(--polymarket-blue);
296
+ border-radius: 4px;
297
+ transition: width 0.3s ease;
298
+ width: 0%;
299
+ }
300
+
301
+ /* Results Section */
302
+ .results-section {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 24px;
306
+ }
307
+
308
+ .results-header {
309
+ display: flex;
310
+ justify-content: space-between;
311
+ align-items: center;
312
+ background: var(--white);
313
+ padding: 28px 32px;
314
+ border-radius: 12px;
315
+ box-shadow: var(--shadow-md);
316
+ border: 1px solid var(--medium-gray);
317
+ }
318
+
319
+ .results-header h2 {
320
+ font-size: 24px;
321
+ font-weight: 600;
322
+ color: var(--text-primary);
323
+ }
324
+
325
+ .export-button {
326
+ background: var(--white);
327
+ color: var(--polymarket-blue);
328
+ border: 1px solid var(--polymarket-blue);
329
+ padding: 10px 20px;
330
+ border-radius: 6px;
331
+ font-size: 14px;
332
+ font-weight: 500;
333
+ cursor: pointer;
334
+ transition: all 0.2s;
335
+ }
336
+
337
+ .export-button:hover {
338
+ background: var(--polymarket-blue);
339
+ color: var(--white);
340
+ }
341
+
342
+ /* Statistics Grid */
343
+ .stats-grid {
344
+ display: grid;
345
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
346
+ gap: 24px;
347
+ }
348
+
349
+ .stat-card {
350
+ background: var(--white);
351
+ border-radius: 12px;
352
+ padding: 28px;
353
+ box-shadow: var(--shadow-md);
354
+ border: 1px solid var(--medium-gray);
355
+ }
356
+
357
+ .stat-header {
358
+ margin-bottom: 20px;
359
+ }
360
+
361
+ .stat-header h3 {
362
+ font-size: 18px;
363
+ font-weight: 600;
364
+ color: var(--text-primary);
365
+ }
366
+
367
+ .stat-content {
368
+ display: flex;
369
+ flex-direction: column;
370
+ gap: 16px;
371
+ }
372
+
373
+ .stat-item {
374
+ display: flex;
375
+ justify-content: space-between;
376
+ align-items: center;
377
+ }
378
+
379
+ .stat-label {
380
+ font-size: 14px;
381
+ color: var(--text-secondary);
382
+ }
383
+
384
+ .stat-value {
385
+ font-size: 16px;
386
+ font-weight: 600;
387
+ color: var(--text-primary);
388
+ }
389
+
390
+ .stat-value.positive {
391
+ color: var(--success);
392
+ }
393
+
394
+ .stat-value.negative {
395
+ color: var(--error);
396
+ }
397
+
398
+ .stat-value.neutral {
399
+ color: var(--warning);
400
+ }
401
+
402
+ /* Charts Grid */
403
+ .charts-grid {
404
+ display: grid;
405
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
406
+ gap: 24px;
407
+ }
408
+
409
+ .chart-card {
410
+ background: var(--white);
411
+ border-radius: 12px;
412
+ padding: 28px;
413
+ box-shadow: var(--shadow-md);
414
+ border: 1px solid var(--medium-gray);
415
+ }
416
+
417
+ .chart-header {
418
+ margin-bottom: 20px;
419
+ }
420
+
421
+ .chart-header h3 {
422
+ font-size: 18px;
423
+ font-weight: 600;
424
+ color: var(--text-primary);
425
+ }
426
+
427
+ .chart-container {
428
+ position: relative;
429
+ height: 300px;
430
+ width: 100%;
431
+ padding: 8px 0;
432
+ }
433
+
434
+ /* Responsive Design */
435
+ @media (max-width: 768px) {
436
+ .nav-container {
437
+ padding: 0 20px;
438
+ height: 64px;
439
+ }
440
+
441
+ .main-container {
442
+ padding: 20px;
443
+ gap: 24px;
444
+ }
445
+
446
+ .controls-section {
447
+ padding: 24px;
448
+ }
449
+
450
+ .controls-grid {
451
+ grid-template-columns: 1fr;
452
+ gap: 20px;
453
+ }
454
+
455
+ .simulation-tabs {
456
+ flex-direction: column;
457
+ gap: 12px;
458
+ }
459
+
460
+ .charts-grid {
461
+ grid-template-columns: 1fr;
462
+ gap: 20px;
463
+ }
464
+
465
+ .chart-card,
466
+ .stat-card {
467
+ padding: 20px;
468
+ }
469
+
470
+ .results-header {
471
+ flex-direction: column;
472
+ gap: 16px;
473
+ align-items: stretch;
474
+ padding: 24px;
475
+ }
476
+ }
477
+
478
+ /* Utility classes */
479
+ .hidden {
480
+ display: none !important;
481
+ }
482
+
483
+ .loading {
484
+ opacity: 0.6;
485
+ pointer-events: none;
486
+ }
487
+
488
+ /* Conditional visibility classes */
489
+ .single-only, .threshold-only, .multi-only {
490
+ transition: all 0.3s ease;
491
+ }
492
+
493
+ .multi-stats, .threshold-stats, .multi-chart {
494
+ transition: all 0.3s ease;
495
+ }