Spaces:
Sleeping
Sleeping
dhruv575 commited on
Commit ·
91f4bb2
1
Parent(s): ed92f24
Implemented
Browse files- Dockerfile +26 -0
- README.md +81 -10
- app.py +208 -0
- data/all_filtered_markets_full_2024_2025.csv +0 -0
- requirements.txt +9 -0
- simulation_utils.py +815 -0
- static/.gitignore +4 -0
- static/README.md +49 -0
- static/index.html +283 -0
- static/package.json +18 -0
- static/polymarket_logo.png +0 -0
- static/script.js +798 -0
- static/styles.css +495 -0
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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|