Spaces:
Sleeping
Sleeping
dhruv575 commited on
Commit ·
febf196
1
Parent(s): 426fb3b
Frontend updates
Browse files- __pycache__/simulation_utils.cpython-314.pyc +0 -0
- static/index.html +423 -310
- static/script.js +263 -647
- static/styles.css +717 -464
__pycache__/simulation_utils.cpython-314.pyc
ADDED
|
Binary file (44.7 kB). View file
|
|
|
static/index.html
CHANGED
|
@@ -4,369 +4,482 @@
|
|
| 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?v=
|
|
|
|
|
|
|
|
|
|
| 8 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 9 |
</head>
|
| 10 |
<body>
|
| 11 |
-
<!--
|
| 12 |
-
<header class="
|
| 13 |
-
<div class="
|
| 14 |
-
<div class="
|
| 15 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
</div>
|
| 17 |
-
<div class="
|
| 18 |
-
<
|
|
|
|
|
|
|
|
|
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
</header>
|
| 22 |
|
| 23 |
-
<!-- Main
|
| 24 |
-
<
|
| 25 |
-
<!--
|
| 26 |
<aside class="sidebar">
|
| 27 |
-
<
|
| 28 |
-
<div class="
|
| 29 |
-
<
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
|
| 37 |
-
<
|
| 38 |
-
<div class="
|
| 39 |
-
<
|
| 40 |
-
|
| 41 |
-
<
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
</div>
|
| 52 |
-
</aside>
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
<
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
|
| 75 |
-
<!--
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
<
|
| 99 |
-
<
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
<
|
| 109 |
-
<
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
-
</div>
|
| 134 |
-
</section>
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
</div>
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
</div>
|
| 146 |
-
</
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
<!-- Summary Statistics -->
|
| 157 |
-
<div class="stats-grid">
|
| 158 |
-
<div class="stat-card">
|
| 159 |
-
<div class="stat-header">
|
| 160 |
-
<h3>Performance</h3>
|
| 161 |
</div>
|
| 162 |
-
<div class="
|
| 163 |
-
<div class="
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
<div class="
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
<div class="
|
| 172 |
-
|
| 173 |
-
<span class="stat-value" id="successRate">--</span>
|
| 174 |
-
</div>
|
| 175 |
</div>
|
| 176 |
</div>
|
| 177 |
|
| 178 |
-
<
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
-
</div>
|
| 197 |
|
| 198 |
-
|
| 199 |
-
<div class="stat-header">
|
| 200 |
<h3>Portfolio Stats</h3>
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
<
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
-
<
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
</div>
|
| 227 |
-
<div class="
|
| 228 |
-
<
|
| 229 |
-
<span class="stat-value" id="avgTimeToTarget">--</span>
|
| 230 |
-
</div>
|
| 231 |
-
<div class="stat-item">
|
| 232 |
-
<span class="stat-label">vs Never Stop:</span>
|
| 233 |
-
<span class="stat-value" id="vsNeverStop">--</span>
|
| 234 |
</div>
|
| 235 |
</div>
|
| 236 |
-
</div>
|
| 237 |
-
</div>
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
<
|
|
|
|
|
|
|
| 244 |
</div>
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
</div>
|
| 248 |
</div>
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
</div>
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
</div>
|
| 257 |
-
</
|
| 258 |
-
|
| 259 |
-
<
|
| 260 |
-
<
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
</div>
|
| 263 |
-
<
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
</div>
|
| 266 |
-
</
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
<
|
| 284 |
-
</
|
| 285 |
-
|
| 286 |
-
<
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
<
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
<p>Markets are filtered to exclude those with inherent variability:</p>
|
| 303 |
-
<ul>
|
| 304 |
-
<li><strong>Sports:</strong> NFL, NBA, MLB, NHL, UFC, Soccer, etc.</li>
|
| 305 |
-
<li><strong>Esports:</strong> LoL, Dota, CS:GO, Valorant, etc.</li>
|
| 306 |
-
<li><strong>Crypto:</strong> Bitcoin, Ethereum, price predictions, etc.</li>
|
| 307 |
-
<li><strong>Weather:</strong> Temperature, rain, storms, etc.</li>
|
| 308 |
-
</ul>
|
| 309 |
-
|
| 310 |
-
<p>For remaining markets, we log price history every 24 hours from 7 days before to 1 day before resolution. The dataset covers 2024-2025 and is available on <a href="https://www.kaggle.com/datasets/dhruvgup/polymarket-closed-2025-markets-7-day-price-history/data" target="_blank">Kaggle</a>.</p>
|
| 311 |
-
|
| 312 |
-
<h3>Dataset Exploration</h3>
|
| 313 |
-
<p><strong>Key Questions Addressed:</strong></p>
|
| 314 |
-
<ol>
|
| 315 |
-
<li><strong>Probability Calibration:</strong> What percentage of markets at different probability thresholds (90-95%, 95-98%, 98-99%, 99-100%) actually resolve to the expected outcome?</li>
|
| 316 |
-
<li><strong>Confidence Intervals:</strong> Win rate and 95% confidence intervals for each probability band</li>
|
| 317 |
-
<li><strong>Time Horizon Analysis:</strong> Comparison of outcomes at 48 hours vs. 7 days before resolution</li>
|
| 318 |
-
<li><strong>Distribution Analysis:</strong> Volume-weighted vs. unweighted win rates</li>
|
| 319 |
-
<li><strong>Expected Value Calculation:</strong> Expected return for each probability band</li>
|
| 320 |
-
</ol>
|
| 321 |
-
|
| 322 |
-
<h3>Simulation Model</h3>
|
| 323 |
-
<div class="sim-type-box">
|
| 324 |
-
<p>The simulation uses a <strong>day-by-day investment decision model</strong>:</p>
|
| 325 |
-
<ol>
|
| 326 |
-
<li>Each day, if not invested, decide to invest with probability <strong>α</strong> (investment_probability)</li>
|
| 327 |
-
<li>If investing, select uniformly at random from markets that resolve exactly `days_before` days from now and meet thresholds</li>
|
| 328 |
-
<li>Remain "locked in" until market resolves, then free to invest again</li>
|
| 329 |
-
<li>Continue until: bust, reach target return, or simulation period ends</li>
|
| 330 |
-
</ol>
|
| 331 |
-
<p><strong>α controls trading frequency:</strong> Low (0.1) = cautious, High (0.2) = aggressive</p>
|
| 332 |
-
</div>
|
| 333 |
-
|
| 334 |
-
<h3>Simulation Types</h3>
|
| 335 |
-
|
| 336 |
-
<div class="sim-type-box">
|
| 337 |
-
<h4>1. Single Fund Simulation</h4>
|
| 338 |
-
<p>A single trader deploys all capital sequentially across safe markets, reinvesting all winnings.</p>
|
| 339 |
-
<p><strong>Parameters:</strong> starting_capital ($10,000), days_before (1-7), min_prob_7d (0.90), min_prob_current (0.90), investment_probability (α), max_duration_days (365)</p>
|
| 340 |
-
</div>
|
| 341 |
-
|
| 342 |
-
<div class="sim-type-box">
|
| 343 |
-
<h4>2. Single Fund Threshold Simulation</h4>
|
| 344 |
-
<p>Identical to Single Fund but with target return thresholds—trading stops when target is reached.</p>
|
| 345 |
-
<p><strong>Benchmarks:</strong> Treasury Rate (4.14%) or NASDAQ Average (10.56%)</p>
|
| 346 |
-
</div>
|
| 347 |
-
|
| 348 |
-
<div class="sim-type-box">
|
| 349 |
-
<h4>3. Multi-Fund Simulation</h4>
|
| 350 |
-
<p>Capital is divided into multiple independent funds, each operating as separate single funds. Tests diversification benefits.</p>
|
| 351 |
-
<p><strong>Parameters:</strong> n_funds (1, 3, 5, or 10 funds), capital divided equally, same α for all funds</p>
|
| 352 |
-
</div>
|
| 353 |
-
|
| 354 |
-
<h3>Technical Implementation</h3>
|
| 355 |
-
<ul>
|
| 356 |
-
<li>Uses actual Polymarket data filtered for 2025+ resolution dates</li>
|
| 357 |
-
<li>Vectorized operations for performance (1000 simulations in ~10-20 seconds)</li>
|
| 358 |
-
<li>Left-skewed exponential market selection using skew_factor</li>
|
| 359 |
-
<li>Binary outcomes: win = 1/probability return, loss = total loss</li>
|
| 360 |
-
<li>Reproducible results via random seeds</li>
|
| 361 |
-
</ul>
|
| 362 |
-
|
| 363 |
-
<h3>Limitations</h3>
|
| 364 |
-
<p>Volume and market movement: If we count the starting capital as $10,000 distributed over 5 funds, we may not need to worry about available liquidity. However, were we to scale this experiment up, this becomes a real concern.</p>
|
| 365 |
-
</div>
|
| 366 |
-
</section>
|
| 367 |
</div>
|
| 368 |
-
</
|
| 369 |
|
| 370 |
-
<script src="/static/script.js?v=
|
| 371 |
</body>
|
| 372 |
</html>
|
|
|
|
| 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?v=10">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
+
<!-- Header -->
|
| 15 |
+
<header class="header">
|
| 16 |
+
<div class="header-content">
|
| 17 |
+
<div class="brand">
|
| 18 |
+
<div class="brand-icon">
|
| 19 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 20 |
+
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
| 21 |
+
<path d="M2 17l10 5 10-5"/>
|
| 22 |
+
<path d="M2 12l10 5 10-5"/>
|
| 23 |
+
</svg>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="brand-text">
|
| 26 |
+
<h1>Safe Choices</h1>
|
| 27 |
+
<span class="brand-tagline">Prediction Market Simulator</span>
|
| 28 |
+
</div>
|
| 29 |
</div>
|
| 30 |
+
<div class="header-meta">
|
| 31 |
+
<div class="data-badge">
|
| 32 |
+
<span class="badge-dot"></span>
|
| 33 |
+
Polymarket 2025
|
| 34 |
+
</div>
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
</header>
|
| 38 |
|
| 39 |
+
<!-- Main Layout -->
|
| 40 |
+
<main class="main">
|
| 41 |
+
<!-- Sidebar -->
|
| 42 |
<aside class="sidebar">
|
| 43 |
+
<!-- View Toggle -->
|
| 44 |
+
<div class="sidebar-section">
|
| 45 |
+
<div class="section-label">View</div>
|
| 46 |
+
<div class="toggle-group">
|
| 47 |
+
<button class="toggle-btn active" data-view="simulation" onclick="selectView('simulation')">
|
| 48 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 49 |
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
| 50 |
+
<path d="M3 9h18"/>
|
| 51 |
+
<path d="M9 21V9"/>
|
| 52 |
+
</svg>
|
| 53 |
+
Simulator
|
| 54 |
+
</button>
|
| 55 |
+
<button class="toggle-btn" data-view="methodology" onclick="selectView('methodology')">
|
| 56 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 57 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
| 58 |
+
<path d="M14 2v6h6"/>
|
| 59 |
+
<path d="M16 13H8"/>
|
| 60 |
+
<path d="M16 17H8"/>
|
| 61 |
+
<path d="M10 9H8"/>
|
| 62 |
+
</svg>
|
| 63 |
+
Methodology
|
| 64 |
+
</button>
|
| 65 |
+
</div>
|
| 66 |
</div>
|
| 67 |
|
| 68 |
+
<!-- Simulation Type -->
|
| 69 |
+
<div class="sidebar-section simulation-view-only">
|
| 70 |
+
<div class="section-label">Strategy</div>
|
| 71 |
+
<div class="strategy-cards">
|
| 72 |
+
<button class="strategy-card active" data-sim="single" onclick="selectSimulation('single')">
|
| 73 |
+
<div class="strategy-icon">
|
| 74 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 75 |
+
<circle cx="12" cy="12" r="10"/>
|
| 76 |
+
<path d="M12 6v6l4 2"/>
|
| 77 |
+
</svg>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="strategy-info">
|
| 80 |
+
<span class="strategy-name">Single Fund</span>
|
| 81 |
+
<span class="strategy-desc">All-in sequential betting</span>
|
| 82 |
+
</div>
|
| 83 |
+
</button>
|
| 84 |
+
<button class="strategy-card" data-sim="threshold" onclick="selectSimulation('threshold')">
|
| 85 |
+
<div class="strategy-icon">
|
| 86 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 87 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
| 88 |
+
<polyline points="22 4 12 14.01 9 11.01"/>
|
| 89 |
+
</svg>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="strategy-info">
|
| 92 |
+
<span class="strategy-name">Target Return</span>
|
| 93 |
+
<span class="strategy-desc">Stop at profit goal</span>
|
| 94 |
+
</div>
|
| 95 |
+
</button>
|
| 96 |
+
<button class="strategy-card" data-sim="multi" onclick="selectSimulation('multi')">
|
| 97 |
+
<div class="strategy-icon">
|
| 98 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 99 |
+
<rect x="3" y="3" width="7" height="7"/>
|
| 100 |
+
<rect x="14" y="3" width="7" height="7"/>
|
| 101 |
+
<rect x="14" y="14" width="7" height="7"/>
|
| 102 |
+
<rect x="3" y="14" width="7" height="7"/>
|
| 103 |
+
</svg>
|
| 104 |
+
</div>
|
| 105 |
+
<div class="strategy-info">
|
| 106 |
+
<span class="strategy-name">Multi Fund</span>
|
| 107 |
+
<span class="strategy-desc">Diversified portfolio</span>
|
| 108 |
+
</div>
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
</div>
|
|
|
|
| 112 |
|
| 113 |
+
<!-- Quick Info -->
|
| 114 |
+
<div class="sidebar-section simulation-view-only">
|
| 115 |
+
<div class="quick-stats">
|
| 116 |
+
<div class="quick-stat">
|
| 117 |
+
<span class="quick-stat-value" id="marketCount">4,265</span>
|
| 118 |
+
<span class="quick-stat-label">Markets</span>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="quick-stat">
|
| 121 |
+
<span class="quick-stat-value">2025</span>
|
| 122 |
+
<span class="quick-stat-label">Dataset</span>
|
| 123 |
+
</div>
|
| 124 |
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</aside>
|
| 127 |
|
| 128 |
+
<!-- Content -->
|
| 129 |
+
<div class="content">
|
| 130 |
+
<!-- Simulation View -->
|
| 131 |
+
<div class="simulation-content" id="simulationContent">
|
| 132 |
+
<!-- Parameters Panel -->
|
| 133 |
+
<section class="panel parameters-panel">
|
| 134 |
+
<div class="panel-header">
|
| 135 |
+
<h2>
|
| 136 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 137 |
+
<circle cx="12" cy="12" r="3"/>
|
| 138 |
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
| 139 |
+
</svg>
|
| 140 |
+
Parameters
|
| 141 |
+
</h2>
|
| 142 |
</div>
|
| 143 |
|
| 144 |
+
<!-- Hidden defaults -->
|
| 145 |
+
<input type="hidden" id="startingCapital" value="10000">
|
| 146 |
+
<input type="hidden" id="startDate" value="2025-01-01">
|
| 147 |
+
<input type="hidden" id="maxDuration" value="365">
|
| 148 |
+
|
| 149 |
+
<div class="params-grid">
|
| 150 |
+
<!-- Core Parameters -->
|
| 151 |
+
<div class="param-group">
|
| 152 |
+
<label class="param-label">
|
| 153 |
+
Simulations
|
| 154 |
+
<span class="param-hint" title="Number of Monte Carlo runs">?</span>
|
| 155 |
+
</label>
|
| 156 |
+
<div class="param-input-wrap">
|
| 157 |
+
<input type="number" id="numSimulations" class="param-input" value="100" min="10" max="100" step="10">
|
| 158 |
+
<span class="param-unit">runs</span>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
|
| 162 |
+
<div class="param-group">
|
| 163 |
+
<label class="param-label">
|
| 164 |
+
Days Before
|
| 165 |
+
<span class="param-hint" title="Days before market closes to invest">?</span>
|
| 166 |
+
</label>
|
| 167 |
+
<div class="param-input-wrap">
|
| 168 |
+
<input type="number" id="daysBefore" class="param-input" value="1" min="1" max="7" step="1">
|
| 169 |
+
<span class="param-unit">days</span>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
|
| 173 |
+
<div class="param-group">
|
| 174 |
+
<label class="param-label">
|
| 175 |
+
Min Prob @ 7d
|
| 176 |
+
<span class="param-hint" title="Minimum probability 7 days before resolution">?</span>
|
| 177 |
+
</label>
|
| 178 |
+
<div class="param-input-wrap">
|
| 179 |
+
<input type="number" id="minProb7d" class="param-input" value="90" min="50" max="99" step="1">
|
| 180 |
+
<span class="param-unit">%</span>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
|
| 184 |
+
<div class="param-group">
|
| 185 |
+
<label class="param-label">
|
| 186 |
+
Min Prob @ Entry
|
| 187 |
+
<span class="param-hint" title="Minimum probability at investment time">?</span>
|
| 188 |
+
</label>
|
| 189 |
+
<div class="param-input-wrap">
|
| 190 |
+
<input type="number" id="minProbCurrent" class="param-input" value="90" min="50" max="99" step="1">
|
| 191 |
+
<span class="param-unit">%</span>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
|
| 195 |
+
<div class="param-group">
|
| 196 |
+
<label class="param-label">
|
| 197 |
+
Trading Frequency
|
| 198 |
+
<span class="param-hint" title="How often to trade when opportunities exist">?</span>
|
| 199 |
+
</label>
|
| 200 |
+
<select id="investmentProbability" class="param-input param-select">
|
| 201 |
+
<option value="0.02">Conservative</option>
|
| 202 |
+
<option value="0.04" selected>Moderate</option>
|
| 203 |
+
<option value="0.06">Aggressive</option>
|
| 204 |
+
</select>
|
| 205 |
+
</div>
|
| 206 |
|
| 207 |
+
<!-- Threshold-specific -->
|
| 208 |
+
<div class="param-group threshold-only">
|
| 209 |
+
<label class="param-label">
|
| 210 |
+
Target Return
|
| 211 |
+
<span class="param-hint" title="Stop trading when this return is reached">?</span>
|
| 212 |
+
</label>
|
| 213 |
+
<select id="targetReturn" class="param-input param-select">
|
| 214 |
+
<option value="4.14">4.14% (Treasury)</option>
|
| 215 |
+
<option value="10.56">10.56% (NASDAQ)</option>
|
| 216 |
+
<option value="custom">Custom...</option>
|
| 217 |
+
</select>
|
| 218 |
+
<input type="number" id="customTarget" class="param-input custom-target-input" min="1" max="100" step="0.1" placeholder="Enter %">
|
| 219 |
+
</div>
|
| 220 |
|
| 221 |
+
<!-- Multi-fund specific -->
|
| 222 |
+
<div class="param-group multi-only">
|
| 223 |
+
<label class="param-label">
|
| 224 |
+
Number of Funds
|
| 225 |
+
<span class="param-hint" title="Split capital across independent funds">?</span>
|
| 226 |
+
</label>
|
| 227 |
+
<div class="param-input-wrap">
|
| 228 |
+
<input type="number" id="numFunds" class="param-input" value="5" min="2" max="10" step="1">
|
| 229 |
+
<span class="param-unit">funds</span>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
</div>
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
<!-- Run Button -->
|
| 235 |
+
<div class="run-container">
|
| 236 |
+
<button class="run-btn" onclick="runSimulation()" id="runBtn">
|
| 237 |
+
<svg class="run-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 238 |
+
<polygon points="5 3 19 12 5 21 5 3"/>
|
| 239 |
+
</svg>
|
| 240 |
+
<span class="run-text">Run Simulation</span>
|
| 241 |
+
<div class="run-spinner"></div>
|
| 242 |
+
</button>
|
| 243 |
+
<span class="run-estimate" id="estimatedTime">~5-10 seconds</span>
|
| 244 |
</div>
|
| 245 |
+
</section>
|
| 246 |
+
|
| 247 |
+
<!-- Progress Bar -->
|
| 248 |
+
<section class="panel progress-panel" id="progressPanel">
|
| 249 |
+
<div class="progress-content">
|
| 250 |
+
<div class="progress-header">
|
| 251 |
+
<span id="progressText">Initializing...</span>
|
| 252 |
+
<span id="progressPercent">0%</span>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="progress-track">
|
| 255 |
+
<div class="progress-fill" id="progressFill"></div>
|
| 256 |
+
</div>
|
| 257 |
</div>
|
| 258 |
+
</section>
|
| 259 |
+
|
| 260 |
+
<!-- Results Section -->
|
| 261 |
+
<section class="results" id="resultsSection">
|
| 262 |
+
<!-- Key Metrics -->
|
| 263 |
+
<div class="metrics-row">
|
| 264 |
+
<div class="metric-card metric-primary">
|
| 265 |
+
<div class="metric-label">Average Return</div>
|
| 266 |
+
<div class="metric-value" id="avgReturn">--</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
</div>
|
| 268 |
+
<div class="metric-card">
|
| 269 |
+
<div class="metric-label">Median Return</div>
|
| 270 |
+
<div class="metric-value" id="medianReturn">--</div>
|
| 271 |
+
</div>
|
| 272 |
+
<div class="metric-card">
|
| 273 |
+
<div class="metric-label">Success Rate</div>
|
| 274 |
+
<div class="metric-value" id="successRate">--</div>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="metric-card metric-risk">
|
| 277 |
+
<div class="metric-label">Bust Rate</div>
|
| 278 |
+
<div class="metric-value" id="bustRate">--</div>
|
|
|
|
|
|
|
| 279 |
</div>
|
| 280 |
</div>
|
| 281 |
|
| 282 |
+
<!-- Secondary Stats -->
|
| 283 |
+
<div class="stats-row">
|
| 284 |
+
<div class="stat-panel">
|
| 285 |
+
<h3>Risk Analysis</h3>
|
| 286 |
+
<div class="stat-list">
|
| 287 |
+
<div class="stat-item">
|
| 288 |
+
<span class="stat-label">Volatility</span>
|
| 289 |
+
<span class="stat-value" id="volatility">--</span>
|
| 290 |
+
</div>
|
| 291 |
+
<div class="stat-item">
|
| 292 |
+
<span class="stat-label">Max Drawdown</span>
|
| 293 |
+
<span class="stat-value" id="maxDrawdown">--</span>
|
| 294 |
+
</div>
|
| 295 |
+
<div class="stat-item">
|
| 296 |
+
<span class="stat-label">5th Percentile</span>
|
| 297 |
+
<span class="stat-value" id="percentile5">--</span>
|
| 298 |
+
</div>
|
| 299 |
+
<div class="stat-item">
|
| 300 |
+
<span class="stat-label">95th Percentile</span>
|
| 301 |
+
<span class="stat-value" id="percentile95">--</span>
|
| 302 |
+
</div>
|
| 303 |
</div>
|
| 304 |
</div>
|
|
|
|
| 305 |
|
| 306 |
+
<div class="stat-panel multi-stats">
|
|
|
|
| 307 |
<h3>Portfolio Stats</h3>
|
| 308 |
+
<div class="stat-list">
|
| 309 |
+
<div class="stat-item">
|
| 310 |
+
<span class="stat-label">Surviving Funds</span>
|
| 311 |
+
<span class="stat-value" id="avgSurvivingFunds">--</span>
|
| 312 |
+
</div>
|
| 313 |
+
<div class="stat-item">
|
| 314 |
+
<span class="stat-label">Survivorship Rate</span>
|
| 315 |
+
<span class="stat-value" id="survivorshipRate">--</span>
|
| 316 |
+
</div>
|
| 317 |
+
<div class="stat-item">
|
| 318 |
+
<span class="stat-label">Diversification</span>
|
| 319 |
+
<span class="stat-value" id="diversificationBenefit">--</span>
|
| 320 |
+
</div>
|
| 321 |
</div>
|
| 322 |
+
</div>
|
| 323 |
+
|
| 324 |
+
<div class="stat-panel threshold-stats">
|
| 325 |
+
<h3>Target Performance</h3>
|
| 326 |
+
<div class="stat-list">
|
| 327 |
+
<div class="stat-item">
|
| 328 |
+
<span class="stat-label">Target Reached</span>
|
| 329 |
+
<span class="stat-value" id="targetReached">--</span>
|
| 330 |
+
</div>
|
| 331 |
+
<div class="stat-item">
|
| 332 |
+
<span class="stat-label">Avg Time to Target</span>
|
| 333 |
+
<span class="stat-value" id="avgTimeToTarget">--</span>
|
| 334 |
+
</div>
|
| 335 |
+
<div class="stat-item">
|
| 336 |
+
<span class="stat-label">vs Never Stop</span>
|
| 337 |
+
<span class="stat-value" id="vsNeverStop">--</span>
|
| 338 |
+
</div>
|
| 339 |
</div>
|
| 340 |
</div>
|
| 341 |
</div>
|
| 342 |
|
| 343 |
+
<!-- Charts -->
|
| 344 |
+
<div class="charts-row">
|
| 345 |
+
<div class="chart-panel">
|
| 346 |
+
<div class="chart-header">
|
| 347 |
+
<h3>Return Distribution</h3>
|
| 348 |
+
<button class="export-btn" onclick="exportResults()" id="exportBtn">
|
| 349 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 350 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
| 351 |
+
<polyline points="7 10 12 15 17 10"/>
|
| 352 |
+
<line x1="12" y1="15" x2="12" y2="3"/>
|
| 353 |
+
</svg>
|
| 354 |
+
Export
|
| 355 |
+
</button>
|
| 356 |
</div>
|
| 357 |
+
<div class="chart-container">
|
| 358 |
+
<canvas id="returnChart"></canvas>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
</div>
|
| 360 |
</div>
|
|
|
|
|
|
|
| 361 |
|
| 362 |
+
<div class="chart-panel">
|
| 363 |
+
<div class="chart-header">
|
| 364 |
+
<h3>Capital Evolution</h3>
|
| 365 |
+
</div>
|
| 366 |
+
<div class="chart-container">
|
| 367 |
+
<canvas id="capitalChart"></canvas>
|
| 368 |
+
</div>
|
| 369 |
</div>
|
| 370 |
+
|
| 371 |
+
<div class="chart-panel multi-chart">
|
| 372 |
+
<div class="chart-header">
|
| 373 |
+
<h3>Fund Survivorship</h3>
|
| 374 |
+
</div>
|
| 375 |
+
<div class="chart-container">
|
| 376 |
+
<canvas id="survivorshipChart"></canvas>
|
| 377 |
+
</div>
|
| 378 |
</div>
|
| 379 |
</div>
|
| 380 |
+
</section>
|
| 381 |
+
</div>
|
| 382 |
|
| 383 |
+
<!-- Methodology View -->
|
| 384 |
+
<div class="methodology-content" id="methodologyContent">
|
| 385 |
+
<article class="article">
|
| 386 |
+
<header class="article-header">
|
| 387 |
+
<h1>Safe Choices</h1>
|
| 388 |
+
<p class="article-subtitle">Assessing the efficiency of "safe bets" on prediction markets</p>
|
| 389 |
+
</header>
|
| 390 |
+
|
| 391 |
+
<section class="article-section">
|
| 392 |
+
<div class="callout callout-intro">
|
| 393 |
+
<p>Imagine 50 hedge funds, each deploying $100 on a "State Earthquake Trade." Each fund bets on their state NOT being hit by an earthquake (10% annual probability), earning 1.11x if correct.</p>
|
| 394 |
+
<p>After 10 years across 100,000 simulations: <strong>17.42 funds survive on average</strong> (range: 4-32). These survivors produced 11% annual returns for a decade. In an efficient market, luck becomes the differentiating factor between investment legends and bankruptcy.</p>
|
| 395 |
</div>
|
| 396 |
+
</section>
|
| 397 |
+
|
| 398 |
+
<section class="article-section">
|
| 399 |
+
<h2>Research Questions</h2>
|
| 400 |
+
<p>We explore the efficiency of "safe choices" (markets trading at 90+ cents before resolution):</p>
|
| 401 |
+
<ul class="article-list">
|
| 402 |
+
<li>How often will traders making random walks across these markets go bust?</li>
|
| 403 |
+
<li>How often will they beat traditional benchmarks?</li>
|
| 404 |
+
<li>Does stopping at a return threshold (e.g., 12%) improve outcomes?</li>
|
| 405 |
+
<li>Does splitting capital across multiple funds improve survivorship?</li>
|
| 406 |
+
</ul>
|
| 407 |
+
<div class="callout callout-hypothesis">
|
| 408 |
+
<strong>Hypothesis:</strong> Inefficiencies in "safe" markets provide opportunities for positive expected value trades exceeding traditional benchmarks like the risk-free rate.
|
| 409 |
</div>
|
| 410 |
+
</section>
|
| 411 |
+
|
| 412 |
+
<section class="article-section">
|
| 413 |
+
<h2>Dataset</h2>
|
| 414 |
+
<p>Data collected via Polymarket's Gamma API and CLOB API, filtering out high-variability categories:</p>
|
| 415 |
+
<div class="tag-group">
|
| 416 |
+
<span class="tag tag-excluded">Sports</span>
|
| 417 |
+
<span class="tag tag-excluded">Esports</span>
|
| 418 |
+
<span class="tag tag-excluded">Crypto Prices</span>
|
| 419 |
+
<span class="tag tag-excluded">Weather</span>
|
| 420 |
</div>
|
| 421 |
+
<p>For remaining markets, we log probability snapshots from 7 days to 1 day before resolution. The dataset covers 2024-2025 and is available on <a href="https://www.kaggle.com/datasets/dhruvgup/polymarket-closed-2025-markets-7-day-price-history/data" target="_blank" rel="noopener">Kaggle</a>.</p>
|
| 422 |
+
</section>
|
| 423 |
+
|
| 424 |
+
<section class="article-section">
|
| 425 |
+
<h2>Simulation Model</h2>
|
| 426 |
+
<div class="model-steps">
|
| 427 |
+
<div class="model-step">
|
| 428 |
+
<span class="step-num">1</span>
|
| 429 |
+
<span class="step-text">Each day, decide to invest with probability <strong>alpha</strong> (trading frequency)</span>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="model-step">
|
| 432 |
+
<span class="step-num">2</span>
|
| 433 |
+
<span class="step-text">Select uniformly at random from eligible markets meeting thresholds</span>
|
| 434 |
+
</div>
|
| 435 |
+
<div class="model-step">
|
| 436 |
+
<span class="step-num">3</span>
|
| 437 |
+
<span class="step-text">Remain locked until resolution, then reinvest winnings</span>
|
| 438 |
+
</div>
|
| 439 |
+
<div class="model-step">
|
| 440 |
+
<span class="step-num">4</span>
|
| 441 |
+
<span class="step-text">Continue until: bust, target reached, or simulation ends</span>
|
| 442 |
+
</div>
|
| 443 |
</div>
|
| 444 |
+
</section>
|
| 445 |
+
|
| 446 |
+
<section class="article-section">
|
| 447 |
+
<h2>Strategies</h2>
|
| 448 |
+
<div class="strategy-explainer">
|
| 449 |
+
<div class="strategy-box">
|
| 450 |
+
<h3>Single Fund</h3>
|
| 451 |
+
<p>Deploy all capital sequentially across safe markets, reinvesting all winnings. High risk, high reward.</p>
|
| 452 |
+
</div>
|
| 453 |
+
<div class="strategy-box">
|
| 454 |
+
<h3>Target Return</h3>
|
| 455 |
+
<p>Same as Single Fund but stop trading once a target return (e.g., Treasury rate or NASDAQ average) is reached.</p>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="strategy-box">
|
| 458 |
+
<h3>Multi Fund</h3>
|
| 459 |
+
<p>Split capital into N independent funds. Tests whether diversification improves survivorship rates.</p>
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
</section>
|
| 463 |
+
|
| 464 |
+
<section class="article-section">
|
| 465 |
+
<h2>Technical Notes</h2>
|
| 466 |
+
<ul class="article-list">
|
| 467 |
+
<li>Uses actual Polymarket data filtered for 2025+ resolution</li>
|
| 468 |
+
<li>Vectorized operations for performance (~1000 sims in 10-20s)</li>
|
| 469 |
+
<li>Binary outcomes: win = 1/probability return, loss = total loss</li>
|
| 470 |
+
<li>Reproducible results via random seeds</li>
|
| 471 |
+
</ul>
|
| 472 |
+
</section>
|
| 473 |
+
|
| 474 |
+
<section class="article-section">
|
| 475 |
+
<h2>Limitations</h2>
|
| 476 |
+
<p>At $10,000 scale distributed across funds, liquidity isn't a concern. However, scaling this strategy significantly would face real liquidity constraints and potential market impact.</p>
|
| 477 |
+
</section>
|
| 478 |
+
</article>
|
| 479 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
</div>
|
| 481 |
+
</main>
|
| 482 |
|
| 483 |
+
<script src="/static/script.js?v=10"></script>
|
| 484 |
</body>
|
| 485 |
</html>
|
static/script.js
CHANGED
|
@@ -1,213 +1,199 @@
|
|
| 1 |
-
//
|
|
|
|
|
|
|
| 2 |
let currentSimType = 'single';
|
|
|
|
| 3 |
let simulationResults = null;
|
| 4 |
let charts = {};
|
| 5 |
|
| 6 |
-
//
|
| 7 |
-
document.addEventListener('DOMContentLoaded',
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
setupEventListeners();
|
| 10 |
updateEstimatedTime();
|
| 11 |
initializeEmptyCharts();
|
| 12 |
-
});
|
| 13 |
-
|
| 14 |
-
// Initialize default values and constraints
|
| 15 |
-
function initializeDefaults() {
|
| 16 |
-
// Set minimum start date and default to 2025-01-01
|
| 17 |
-
const startDateInput = document.getElementById('startDate');
|
| 18 |
-
const defaultDate = new Date('2025-01-01');
|
| 19 |
-
|
| 20 |
-
startDateInput.value = defaultDate.toISOString().split('T')[0];
|
| 21 |
-
startDateInput.min = '2025-01-01';
|
| 22 |
-
|
| 23 |
-
// Initialize target return dropdown handler
|
| 24 |
-
document.getElementById('targetReturn').addEventListener('change', function() {
|
| 25 |
-
const customInput = document.getElementById('customTarget');
|
| 26 |
-
if (this.value === 'custom') {
|
| 27 |
-
customInput.style.display = 'block';
|
| 28 |
-
customInput.value = '12.0';
|
| 29 |
-
} else {
|
| 30 |
-
customInput.style.display = 'none';
|
| 31 |
-
}
|
| 32 |
-
});
|
| 33 |
}
|
| 34 |
|
| 35 |
-
// Setup event listeners
|
| 36 |
function setupEventListeners() {
|
| 37 |
-
//
|
| 38 |
-
document.getElementById('numSimulations')
|
| 39 |
-
|
|
|
|
| 40 |
updateEstimatedTime();
|
| 41 |
});
|
| 42 |
-
|
| 43 |
-
document.getElementById('numFunds')
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
});
|
| 46 |
-
|
| 47 |
-
//
|
| 48 |
-
const
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
});
|
| 52 |
-
|
| 53 |
-
//
|
| 54 |
-
|
| 55 |
-
inputs.forEach(input => {
|
| 56 |
input.addEventListener('input', validateInputs);
|
| 57 |
});
|
| 58 |
}
|
| 59 |
|
| 60 |
-
//
|
| 61 |
-
let currentView = 'simulation';
|
| 62 |
-
|
| 63 |
-
// Select view (simulation or methodology)
|
| 64 |
function selectView(view) {
|
| 65 |
currentView = view;
|
| 66 |
-
|
| 67 |
-
// Update
|
| 68 |
-
document.querySelectorAll('.
|
| 69 |
-
|
| 70 |
});
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
const
|
| 75 |
-
|
| 76 |
-
|
| 77 |
if (view === 'simulation') {
|
| 78 |
-
|
| 79 |
-
|
| 80 |
document.querySelectorAll('.simulation-view-only').forEach(el => {
|
| 81 |
el.classList.remove('hidden');
|
| 82 |
});
|
| 83 |
} else {
|
| 84 |
-
|
| 85 |
-
|
| 86 |
document.querySelectorAll('.simulation-view-only').forEach(el => {
|
| 87 |
el.classList.add('hidden');
|
| 88 |
});
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
| 92 |
-
//
|
| 93 |
function selectSimulation(type) {
|
| 94 |
currentSimType = type;
|
| 95 |
-
|
| 96 |
-
// Update
|
| 97 |
-
document.querySelectorAll('.
|
| 98 |
-
|
| 99 |
});
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
// Show/hide conditional controls using classes
|
| 103 |
document.querySelectorAll('.threshold-only').forEach(el => {
|
| 104 |
el.classList.toggle('visible', type === 'threshold');
|
| 105 |
});
|
| 106 |
-
|
| 107 |
document.querySelectorAll('.multi-only').forEach(el => {
|
| 108 |
el.classList.toggle('visible', type === 'multi');
|
| 109 |
});
|
| 110 |
-
|
| 111 |
-
updateEstimatedTime();
|
| 112 |
-
}
|
| 113 |
|
| 114 |
-
|
| 115 |
-
function updateEstimatedTime() {
|
| 116 |
-
const numSims = parseInt(document.getElementById('numSimulations').value) || 100;
|
| 117 |
-
const maxDuration = parseInt(document.getElementById('maxDuration').value) || 365;
|
| 118 |
-
const numFunds = currentSimType === 'multi' ? (parseInt(document.getElementById('numFunds').value) || 5) : 1;
|
| 119 |
-
|
| 120 |
-
// Rough estimation based on complexity
|
| 121 |
-
let baseTime = numSims * 0.05; // ~50ms per simulation
|
| 122 |
-
baseTime *= (maxDuration / 365); // Scale by duration
|
| 123 |
-
if (currentSimType === 'multi') {
|
| 124 |
-
baseTime *= Math.sqrt(numFunds); // Multi-fund overhead
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
const estimatedSeconds = Math.max(2, Math.ceil(baseTime));
|
| 128 |
-
|
| 129 |
-
let timeText = '';
|
| 130 |
-
if (estimatedSeconds < 60) {
|
| 131 |
-
timeText = `~${estimatedSeconds} seconds`;
|
| 132 |
-
} else {
|
| 133 |
-
const minutes = Math.ceil(estimatedSeconds / 60);
|
| 134 |
-
timeText = `~${minutes} minute${minutes > 1 ? 's' : ''}`;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
document.getElementById('estimatedTime').textContent = `Estimated time: ${timeText}`;
|
| 138 |
}
|
| 139 |
|
| 140 |
-
//
|
| 141 |
function validateInputs() {
|
| 142 |
const errors = [];
|
| 143 |
-
|
| 144 |
-
// Check required fields
|
| 145 |
-
const startingCapital = parseFloat(document.getElementById('startingCapital').value);
|
| 146 |
-
if (!startingCapital || startingCapital < 1000) {
|
| 147 |
-
errors.push('Starting capital must be at least $1,000');
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
const startDate = new Date(document.getElementById('startDate').value);
|
| 151 |
-
const minDate = new Date('2025-01-01');
|
| 152 |
-
if (startDate < minDate) {
|
| 153 |
-
errors.push('Start date must be 2025-01-01 or later');
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
const minProb7d = parseFloat(document.getElementById('minProb7d').value);
|
| 157 |
const minProbCurrent = parseFloat(document.getElementById('minProbCurrent').value);
|
|
|
|
| 158 |
if (minProb7d < 50 || minProb7d > 99) {
|
| 159 |
errors.push('7-day probability must be between 50% and 99%');
|
| 160 |
}
|
| 161 |
if (minProbCurrent < 50 || minProbCurrent > 99) {
|
| 162 |
errors.push('Current probability must be between 50% and 99%');
|
| 163 |
}
|
| 164 |
-
|
| 165 |
-
// Update run button state
|
| 166 |
const runBtn = document.getElementById('runBtn');
|
| 167 |
runBtn.disabled = errors.length > 0;
|
| 168 |
-
|
| 169 |
return errors.length === 0;
|
| 170 |
}
|
| 171 |
|
| 172 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
async function runSimulation() {
|
| 174 |
if (!validateInputs()) return;
|
| 175 |
-
|
| 176 |
-
// Get parameters
|
| 177 |
const params = getSimulationParameters();
|
| 178 |
-
|
| 179 |
-
// Update UI for running state
|
| 180 |
showProgress();
|
| 181 |
disableControls();
|
| 182 |
-
|
| 183 |
try {
|
| 184 |
-
// Call the backend API
|
| 185 |
const results = await callSimulationAPI(params);
|
| 186 |
-
|
| 187 |
-
// Store results and display
|
| 188 |
simulationResults = results;
|
| 189 |
displayResults(results);
|
| 190 |
-
|
| 191 |
} catch (error) {
|
| 192 |
console.error('Simulation error:', error);
|
| 193 |
-
alert('
|
| 194 |
} finally {
|
| 195 |
hideProgress();
|
| 196 |
enableControls();
|
| 197 |
}
|
| 198 |
}
|
| 199 |
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
async function callSimulationAPI(params) {
|
| 202 |
const progressFill = document.getElementById('progressFill');
|
| 203 |
const progressPercent = document.getElementById('progressPercent');
|
| 204 |
const progressText = document.getElementById('progressText');
|
| 205 |
-
|
| 206 |
-
progressText.textContent = '
|
| 207 |
progressFill.style.width = '10%';
|
| 208 |
progressPercent.textContent = '10%';
|
| 209 |
-
|
| 210 |
-
// Prepare request body
|
| 211 |
const requestBody = {
|
| 212 |
simType: params.simType,
|
| 213 |
startingCapital: params.startingCapital,
|
|
@@ -219,362 +205,161 @@ async function callSimulationAPI(params) {
|
|
| 219 |
daysBefore: params.daysBefore,
|
| 220 |
investmentProbability: params.investmentProbability
|
| 221 |
};
|
| 222 |
-
|
| 223 |
-
if (params.
|
| 224 |
requestBody.targetReturn = params.targetReturn;
|
| 225 |
}
|
| 226 |
-
|
| 227 |
-
if (params.simType === 'multi' && params.numFunds !== undefined) {
|
| 228 |
requestBody.numFunds = params.numFunds;
|
| 229 |
}
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
| 232 |
const response = await fetch('/api/simulate', {
|
| 233 |
method: 'POST',
|
| 234 |
-
headers: {
|
| 235 |
-
'Content-Type': 'application/json',
|
| 236 |
-
},
|
| 237 |
body: JSON.stringify(requestBody)
|
| 238 |
});
|
| 239 |
-
|
| 240 |
if (!response.ok) {
|
| 241 |
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
| 242 |
-
throw new Error(errorData.detail || `HTTP ${response.status}
|
| 243 |
}
|
| 244 |
-
|
| 245 |
progressText.textContent = 'Processing results...';
|
| 246 |
-
progressFill.style.width = '
|
| 247 |
-
progressPercent.textContent = '
|
| 248 |
-
|
| 249 |
const results = await response.json();
|
| 250 |
-
|
| 251 |
progressFill.style.width = '100%';
|
| 252 |
progressPercent.textContent = '100%';
|
| 253 |
progressText.textContent = 'Complete!';
|
| 254 |
-
|
| 255 |
-
return results;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Get simulation parameters from UI
|
| 259 |
-
function getSimulationParameters() {
|
| 260 |
-
const params = {
|
| 261 |
-
simType: currentSimType,
|
| 262 |
-
startingCapital: parseFloat(document.getElementById('startingCapital').value),
|
| 263 |
-
numSimulations: parseInt(document.getElementById('numSimulations').value),
|
| 264 |
-
startDate: document.getElementById('startDate').value,
|
| 265 |
-
maxDuration: parseInt(document.getElementById('maxDuration').value),
|
| 266 |
-
minProb7d: parseFloat(document.getElementById('minProb7d').value) / 100,
|
| 267 |
-
minProbCurrent: parseFloat(document.getElementById('minProbCurrent').value) / 100,
|
| 268 |
-
daysBefore: parseInt(document.getElementById('daysBefore').value),
|
| 269 |
-
investmentProbability: parseFloat(document.getElementById('investmentProbability').value)
|
| 270 |
-
};
|
| 271 |
-
|
| 272 |
-
// Type-specific parameters
|
| 273 |
-
if (currentSimType === 'threshold') {
|
| 274 |
-
const targetSelect = document.getElementById('targetReturn').value;
|
| 275 |
-
if (targetSelect === 'custom') {
|
| 276 |
-
params.targetReturn = parseFloat(document.getElementById('customTarget').value) / 100;
|
| 277 |
-
} else {
|
| 278 |
-
params.targetReturn = parseFloat(targetSelect) / 100;
|
| 279 |
-
}
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
if (currentSimType === 'multi') {
|
| 283 |
-
params.numFunds = parseInt(document.getElementById('numFunds').value);
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
return params;
|
| 287 |
-
}
|
| 288 |
|
| 289 |
-
|
| 290 |
-
async function simulateComputation(params) {
|
| 291 |
-
// This function is replaced by callSimulationAPI
|
| 292 |
-
// Kept for fallback if needed
|
| 293 |
-
return await callSimulationAPI(params);
|
| 294 |
}
|
| 295 |
|
| 296 |
-
//
|
| 297 |
-
function
|
| 298 |
-
const
|
| 299 |
-
|
| 300 |
-
const run = {
|
| 301 |
-
finalCapital: 0,
|
| 302 |
-
totalReturn: 0,
|
| 303 |
-
numTrades: 0,
|
| 304 |
-
wentBust: false,
|
| 305 |
-
reachedTarget: false,
|
| 306 |
-
simulationDays: 0
|
| 307 |
-
};
|
| 308 |
-
|
| 309 |
-
if (params.simType === 'multi') {
|
| 310 |
-
run.survivingFunds = 0;
|
| 311 |
-
run.fundResults = [];
|
| 312 |
-
|
| 313 |
-
for (let f = 0; f < params.numFunds; f++) {
|
| 314 |
-
const fundRun = generateSingleFundRun(params, rng.next());
|
| 315 |
-
run.fundResults.push(fundRun);
|
| 316 |
-
if (!fundRun.wentBust) run.survivingFunds++;
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
run.finalCapital = run.fundResults.reduce((sum, fund) => sum + fund.finalCapital, 0);
|
| 320 |
-
run.totalReturn = (run.finalCapital - params.startingCapital) / params.startingCapital;
|
| 321 |
-
run.numTrades = run.fundResults.reduce((sum, fund) => sum + fund.numTrades, 0);
|
| 322 |
-
run.wentBust = run.survivingFunds === 0;
|
| 323 |
-
} else {
|
| 324 |
-
const singleRun = generateSingleFundRun(params, seed);
|
| 325 |
-
Object.assign(run, singleRun);
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
return run;
|
| 329 |
-
}
|
| 330 |
|
| 331 |
-
//
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
const run = {
|
| 337 |
-
finalCapital: capital,
|
| 338 |
-
totalReturn: 0,
|
| 339 |
-
numTrades: 0,
|
| 340 |
-
wentBust: false,
|
| 341 |
-
reachedTarget: false,
|
| 342 |
-
simulationDays: 0,
|
| 343 |
-
capitalHistory: [capital]
|
| 344 |
-
};
|
| 345 |
-
|
| 346 |
-
const maxTrades = Math.floor(params.maxDuration / 7) + rng.nextInt(5, 20);
|
| 347 |
-
let day = 0;
|
| 348 |
-
|
| 349 |
-
for (let trade = 0; trade < maxTrades && capital > 0 && day < params.maxDuration; trade++) {
|
| 350 |
-
// Check target return for threshold simulations
|
| 351 |
-
if (params.simType === 'threshold' && params.targetReturn) {
|
| 352 |
-
const currentReturn = (capital - (params.startingCapital / (params.numFunds || 1))) / (params.startingCapital / (params.numFunds || 1));
|
| 353 |
-
if (currentReturn >= params.targetReturn) {
|
| 354 |
-
run.reachedTarget = true;
|
| 355 |
-
break;
|
| 356 |
-
}
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
// Market probability based on thresholds (realistic distribution)
|
| 360 |
-
const marketProb = Math.max(params.minProbCurrent,
|
| 361 |
-
params.minProbCurrent + rng.nextGaussian() * 0.05);
|
| 362 |
-
|
| 363 |
-
// Win probability (slightly higher than market prob to simulate edge)
|
| 364 |
-
const winProb = Math.min(0.99, marketProb + 0.01 + rng.nextGaussian() * 0.02);
|
| 365 |
-
|
| 366 |
-
const won = rng.nextFloat() < winProb;
|
| 367 |
-
|
| 368 |
-
if (won) {
|
| 369 |
-
capital = capital / marketProb; // Return = 1/probability
|
| 370 |
-
} else {
|
| 371 |
-
capital = 0; // Total loss
|
| 372 |
-
run.wentBust = true;
|
| 373 |
-
break;
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
run.numTrades++;
|
| 377 |
-
run.capitalHistory.push(capital);
|
| 378 |
-
|
| 379 |
-
// Advance time (realistic trading frequency)
|
| 380 |
-
day += rng.nextInt(1, 14);
|
| 381 |
-
|
| 382 |
-
// Ending factor check
|
| 383 |
-
const endingFactor = 0.05 + (day * 0.001);
|
| 384 |
-
if (rng.nextFloat() < endingFactor) break;
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
run.finalCapital = capital;
|
| 388 |
-
run.simulationDays = day;
|
| 389 |
-
const initialCapital = params.simType === 'multi' ? params.startingCapital / params.numFunds : params.startingCapital;
|
| 390 |
-
run.totalReturn = (capital - initialCapital) / initialCapital;
|
| 391 |
-
|
| 392 |
-
return run;
|
| 393 |
-
}
|
| 394 |
|
| 395 |
-
//
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
const summary = {
|
| 402 |
-
avgReturn: mean(returns),
|
| 403 |
-
medianReturn: median(returns),
|
| 404 |
-
returnVolatility: standardDeviation(returns),
|
| 405 |
-
avgFinalCapital: mean(capitals),
|
| 406 |
-
medianFinalCapital: median(capitals),
|
| 407 |
-
bustRate: runs.filter(r => r.wentBust).length / runs.length,
|
| 408 |
-
positiveReturnRate: returns.filter(r => r > 0).length / returns.length,
|
| 409 |
-
avgTrades: mean(trades),
|
| 410 |
-
|
| 411 |
-
// Percentiles
|
| 412 |
-
return5th: percentile(returns, 5),
|
| 413 |
-
return95th: percentile(returns, 95),
|
| 414 |
-
|
| 415 |
-
// Max drawdown approximation
|
| 416 |
-
maxDrawdown: Math.abs(Math.min(...returns))
|
| 417 |
-
};
|
| 418 |
-
|
| 419 |
-
// Type-specific stats
|
| 420 |
-
if (params.simType === 'threshold' && params.targetReturn) {
|
| 421 |
-
const reachedTarget = runs.filter(r => r.reachedTarget).length;
|
| 422 |
-
summary.targetReachedRate = reachedTarget / runs.length;
|
| 423 |
-
summary.avgTimeToTarget = mean(runs.filter(r => r.reachedTarget).map(r => r.simulationDays)) || 0;
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
if (params.simType === 'multi') {
|
| 427 |
-
const survivingFunds = runs.map(r => r.survivingFunds);
|
| 428 |
-
summary.avgSurvivingFunds = mean(survivingFunds);
|
| 429 |
-
summary.survivorshipRate = mean(survivingFunds) / params.numFunds;
|
| 430 |
-
summary.portfolioBustRate = runs.filter(r => r.survivingFunds === 0).length / runs.length;
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
return summary;
|
| 434 |
-
}
|
| 435 |
|
| 436 |
-
//
|
| 437 |
-
function displayResults(results) {
|
| 438 |
-
const { summary, parameters } = results;
|
| 439 |
-
|
| 440 |
-
// Show results section (always visible, just update content)
|
| 441 |
-
document.getElementById('resultsSection').style.display = 'block';
|
| 442 |
-
document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });
|
| 443 |
-
|
| 444 |
-
// Update basic statistics
|
| 445 |
-
document.getElementById('avgReturn').textContent = formatPercentage(summary.avgReturn);
|
| 446 |
-
document.getElementById('avgReturn').className = `stat-value ${getReturnClass(summary.avgReturn)}`;
|
| 447 |
-
|
| 448 |
-
document.getElementById('medianReturn').textContent = formatPercentage(summary.medianReturn);
|
| 449 |
-
document.getElementById('medianReturn').className = `stat-value ${getReturnClass(summary.medianReturn)}`;
|
| 450 |
-
|
| 451 |
-
document.getElementById('successRate').textContent = formatPercentage(summary.positiveReturnRate);
|
| 452 |
-
|
| 453 |
-
document.getElementById('bustRate').textContent = formatPercentage(summary.bustRate);
|
| 454 |
-
document.getElementById('bustRate').className = `stat-value ${summary.bustRate > 0.1 ? 'negative' : 'positive'}`;
|
| 455 |
-
|
| 456 |
document.getElementById('volatility').textContent = formatPercentage(summary.returnVolatility);
|
| 457 |
document.getElementById('maxDrawdown').textContent = formatPercentage(summary.maxDrawdown);
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
| 460 |
if (parameters.simType === 'multi') {
|
| 461 |
document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
|
| 462 |
el.classList.add('visible');
|
| 463 |
});
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
document.getElementById('
|
| 468 |
-
|
| 469 |
-
// Simple diversification benefit calculation
|
| 470 |
-
const diversificationBenefit = summary.returnVolatility < 0.3 ? 'Positive' : 'Limited';
|
| 471 |
-
document.getElementById('diversificationBenefit').textContent = diversificationBenefit;
|
| 472 |
} else {
|
| 473 |
document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
|
| 474 |
el.classList.remove('visible');
|
| 475 |
});
|
| 476 |
}
|
| 477 |
-
|
| 478 |
if (parameters.simType === 'threshold') {
|
| 479 |
document.querySelectorAll('.threshold-stats').forEach(el => {
|
| 480 |
el.classList.add('visible');
|
| 481 |
});
|
| 482 |
-
|
| 483 |
document.getElementById('targetReached').textContent = formatPercentage(summary.targetReachedRate || 0);
|
| 484 |
-
document.getElementById('avgTimeToTarget').textContent =
|
| 485 |
summary.avgTimeToTarget ? `${Math.round(summary.avgTimeToTarget)} days` : 'N/A';
|
| 486 |
-
document.getElementById('vsNeverStop').textContent =
|
| 487 |
-
summary.targetReachedRate > 0.5 ? 'Better' : 'Similar';
|
| 488 |
} else {
|
| 489 |
document.querySelectorAll('.threshold-stats').forEach(el => {
|
| 490 |
el.classList.remove('visible');
|
| 491 |
});
|
| 492 |
}
|
| 493 |
-
|
| 494 |
// Generate charts
|
| 495 |
generateCharts(results);
|
| 496 |
}
|
| 497 |
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
function initializeEmptyCharts() {
|
| 500 |
-
|
| 501 |
-
charts.
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
}
|
| 509 |
|
| 510 |
-
// Generate all charts
|
| 511 |
function generateCharts(results) {
|
| 512 |
-
const { runs,
|
| 513 |
-
|
| 514 |
// Destroy existing charts
|
| 515 |
-
Object.values(charts).forEach(chart =>
|
| 516 |
-
if (chart) chart.destroy();
|
| 517 |
-
});
|
| 518 |
charts = {};
|
| 519 |
-
|
| 520 |
-
// Return distribution
|
| 521 |
charts.return = createReturnDistributionChart(runs);
|
| 522 |
-
|
| 523 |
-
// Capital evolution
|
| 524 |
charts.capital = createCapitalEvolutionChart(runs);
|
| 525 |
-
|
| 526 |
-
//
|
| 527 |
if (parameters.simType === 'multi') {
|
| 528 |
charts.survivorship = createSurvivorshipChart(runs, parameters.numFunds);
|
| 529 |
} else {
|
| 530 |
-
charts.survivorship =
|
| 531 |
}
|
| 532 |
}
|
| 533 |
|
| 534 |
-
// Create empty return chart with default range
|
| 535 |
-
function createEmptyReturnChart() {
|
| 536 |
-
const ctx = document.getElementById('returnChart').getContext('2d');
|
| 537 |
-
const defaultBins = [];
|
| 538 |
-
for (let i = -100; i <= 100; i += 2) {
|
| 539 |
-
defaultBins.push(i);
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
return new Chart(ctx, {
|
| 543 |
-
type: 'bar',
|
| 544 |
-
data: {
|
| 545 |
-
labels: defaultBins.map(b => `${b}%`),
|
| 546 |
-
datasets: [{
|
| 547 |
-
label: 'Frequency',
|
| 548 |
-
data: new Array(defaultBins.length).fill(0),
|
| 549 |
-
backgroundColor: 'rgba(86, 175, 226, 0.3)',
|
| 550 |
-
borderColor: '#56AFE2',
|
| 551 |
-
borderWidth: 1
|
| 552 |
-
}]
|
| 553 |
-
},
|
| 554 |
-
options: getChartOptions('Return (%)', 'Frequency')
|
| 555 |
-
});
|
| 556 |
-
}
|
| 557 |
-
|
| 558 |
-
// Create return distribution chart with smart 2% bins
|
| 559 |
function createReturnDistributionChart(runs) {
|
| 560 |
const ctx = document.getElementById('returnChart').getContext('2d');
|
| 561 |
const returns = runs.map(r => r.totalReturn * 100);
|
| 562 |
-
|
| 563 |
-
// Calculate smart range based on data
|
| 564 |
const minReturn = Math.min(...returns);
|
| 565 |
const maxReturn = Math.max(...returns);
|
| 566 |
-
|
| 567 |
-
// Round to nearest 10% for cleaner bounds, with padding
|
| 568 |
const binStart = Math.floor(minReturn / 10) * 10;
|
| 569 |
-
const binEnd = Math.min(Math.ceil(maxReturn / 10) * 10, 200);
|
| 570 |
-
|
| 571 |
-
// Generate 2% bins within the smart range
|
| 572 |
const bins = [];
|
| 573 |
-
for (let i = binStart; i <= binEnd; i +=
|
| 574 |
-
|
| 575 |
-
}
|
| 576 |
-
|
| 577 |
-
// Count returns in bins
|
| 578 |
const binCounts = new Array(bins.length).fill(0);
|
| 579 |
returns.forEach(ret => {
|
| 580 |
for (let i = 0; i < bins.length - 1; i++) {
|
|
@@ -583,12 +368,9 @@ function createReturnDistributionChart(runs) {
|
|
| 583 |
break;
|
| 584 |
}
|
| 585 |
}
|
| 586 |
-
|
| 587 |
-
if (ret >= bins[bins.length - 1]) {
|
| 588 |
-
binCounts[bins.length - 1]++;
|
| 589 |
-
}
|
| 590 |
});
|
| 591 |
-
|
| 592 |
return new Chart(ctx, {
|
| 593 |
type: 'bar',
|
| 594 |
data: {
|
|
@@ -596,112 +378,69 @@ function createReturnDistributionChart(runs) {
|
|
| 596 |
datasets: [{
|
| 597 |
label: 'Frequency',
|
| 598 |
data: binCounts,
|
| 599 |
-
backgroundColor: 'rgba(
|
| 600 |
-
borderColor: '
|
| 601 |
-
borderWidth: 1
|
|
|
|
| 602 |
}]
|
| 603 |
},
|
| 604 |
-
options: getChartOptions('
|
| 605 |
-
});
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
// Create empty capital evolution chart
|
| 609 |
-
function createEmptyCapitalChart() {
|
| 610 |
-
const ctx = document.getElementById('capitalChart').getContext('2d');
|
| 611 |
-
return new Chart(ctx, {
|
| 612 |
-
type: 'line',
|
| 613 |
-
data: {
|
| 614 |
-
labels: [],
|
| 615 |
-
datasets: []
|
| 616 |
-
},
|
| 617 |
-
options: getLineChartOptions('Trade Number', 'Capital ($)')
|
| 618 |
});
|
| 619 |
}
|
| 620 |
|
| 621 |
-
// Create capital evolution chart
|
| 622 |
function createCapitalEvolutionChart(runs) {
|
| 623 |
const ctx = document.getElementById('capitalChart').getContext('2d');
|
| 624 |
-
|
| 625 |
-
// Distinct color palette for capital evolution
|
| 626 |
const colors = [
|
| 627 |
-
{ border: '#
|
| 628 |
-
{ border: '#
|
| 629 |
-
{ border: '#
|
| 630 |
-
{ border: '#
|
| 631 |
-
{ border: '#
|
| 632 |
];
|
| 633 |
-
|
| 634 |
-
// Sample up to 5 runs
|
| 635 |
const sampleRuns = runs.slice(0, 5);
|
| 636 |
const datasets = sampleRuns.map((run, i) => {
|
| 637 |
-
// Use capitalHistory if available
|
| 638 |
let data = [];
|
| 639 |
-
if (run.capitalHistory
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
return { x: day, y: capital };
|
| 645 |
-
});
|
| 646 |
} else {
|
| 647 |
-
|
| 648 |
-
data = [
|
| 649 |
-
{ x: 0, y: 10000 }, // Starting capital
|
| 650 |
-
{ x: 1, y: run.finalCapital }
|
| 651 |
-
];
|
| 652 |
}
|
| 653 |
-
|
| 654 |
return {
|
| 655 |
label: `Run ${i + 1}`,
|
| 656 |
data: data,
|
| 657 |
-
borderColor: colors[i
|
| 658 |
-
backgroundColor: colors[i
|
| 659 |
borderWidth: 2,
|
| 660 |
fill: false,
|
| 661 |
-
tension: 0.3,
|
| 662 |
-
pointRadius: 0
|
| 663 |
};
|
| 664 |
});
|
| 665 |
-
|
| 666 |
return new Chart(ctx, {
|
| 667 |
type: 'line',
|
| 668 |
data: { datasets },
|
| 669 |
-
options:
|
| 670 |
-
});
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
// Create empty survivorship chart
|
| 674 |
-
function createEmptySurvivorshipChart() {
|
| 675 |
-
const ctx = document.getElementById('survivorshipChart').getContext('2d');
|
| 676 |
-
return new Chart(ctx, {
|
| 677 |
-
type: 'bar',
|
| 678 |
-
data: {
|
| 679 |
-
labels: [],
|
| 680 |
-
datasets: [{
|
| 681 |
-
label: 'Frequency',
|
| 682 |
-
data: [],
|
| 683 |
-
backgroundColor: 'rgba(135, 191, 255, 0.15)',
|
| 684 |
-
borderColor: '#87BFFF',
|
| 685 |
-
borderWidth: 1
|
| 686 |
-
}]
|
| 687 |
-
},
|
| 688 |
-
options: getChartOptions('Surviving Funds', 'Frequency')
|
| 689 |
});
|
| 690 |
}
|
| 691 |
|
| 692 |
-
// Create survivorship chart for multi-fund
|
| 693 |
function createSurvivorshipChart(runs, numFunds) {
|
| 694 |
const ctx = document.getElementById('survivorshipChart').getContext('2d');
|
| 695 |
const survivingCounts = runs.map(r => r.survivingFunds);
|
| 696 |
-
|
| 697 |
-
// Preset bins: 0 to numFunds
|
| 698 |
const labels = [];
|
| 699 |
const data = [];
|
| 700 |
for (let i = 0; i <= numFunds; i++) {
|
| 701 |
labels.push(i.toString());
|
| 702 |
data.push(survivingCounts.filter(c => c === i).length);
|
| 703 |
}
|
| 704 |
-
|
| 705 |
return new Chart(ctx, {
|
| 706 |
type: 'bar',
|
| 707 |
data: {
|
|
@@ -709,178 +448,105 @@ function createSurvivorshipChart(runs, numFunds) {
|
|
| 709 |
datasets: [{
|
| 710 |
label: 'Frequency',
|
| 711 |
data: data,
|
| 712 |
-
backgroundColor: 'rgba(
|
| 713 |
-
borderColor: '
|
| 714 |
-
borderWidth: 1
|
|
|
|
| 715 |
}]
|
| 716 |
},
|
| 717 |
-
options: getChartOptions('
|
| 718 |
});
|
| 719 |
}
|
| 720 |
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
return {
|
| 724 |
-
responsive: true,
|
| 725 |
-
maintainAspectRatio: false,
|
| 726 |
-
plugins: {
|
| 727 |
-
legend: { display: false }
|
| 728 |
-
},
|
| 729 |
-
scales: {
|
| 730 |
-
x: {
|
| 731 |
-
title: {
|
| 732 |
-
display: true,
|
| 733 |
-
text: xLabel,
|
| 734 |
-
color: '#87BFFF',
|
| 735 |
-
font: { size: 12 }
|
| 736 |
-
},
|
| 737 |
-
grid: {
|
| 738 |
-
color: 'rgba(135, 191, 255, 0.1)',
|
| 739 |
-
drawBorder: false
|
| 740 |
-
},
|
| 741 |
-
ticks: {
|
| 742 |
-
color: '#87BFFF',
|
| 743 |
-
font: { size: 11 }
|
| 744 |
-
}
|
| 745 |
-
},
|
| 746 |
-
y: {
|
| 747 |
-
title: {
|
| 748 |
-
display: true,
|
| 749 |
-
text: yLabel,
|
| 750 |
-
color: '#87BFFF',
|
| 751 |
-
font: { size: 12 }
|
| 752 |
-
},
|
| 753 |
-
grid: {
|
| 754 |
-
color: 'rgba(135, 191, 255, 0.1)',
|
| 755 |
-
drawBorder: false
|
| 756 |
-
},
|
| 757 |
-
ticks: {
|
| 758 |
-
color: '#87BFFF',
|
| 759 |
-
font: { size: 11 }
|
| 760 |
-
}
|
| 761 |
-
}
|
| 762 |
-
}
|
| 763 |
-
};
|
| 764 |
-
}
|
| 765 |
-
|
| 766 |
-
// Helper function for line chart options
|
| 767 |
-
function getLineChartOptions(xLabel, yLabel) {
|
| 768 |
-
return {
|
| 769 |
responsive: true,
|
| 770 |
maintainAspectRatio: false,
|
| 771 |
plugins: {
|
| 772 |
-
legend: {
|
| 773 |
-
display:
|
| 774 |
labels: {
|
| 775 |
-
color: '#
|
| 776 |
font: { size: 11 },
|
| 777 |
usePointStyle: true,
|
| 778 |
-
padding:
|
| 779 |
}
|
| 780 |
}
|
| 781 |
},
|
| 782 |
scales: {
|
| 783 |
x: {
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
display: true,
|
| 787 |
-
text: xLabel,
|
| 788 |
-
color: '#87BFFF',
|
| 789 |
-
font: { size: 12 }
|
| 790 |
-
},
|
| 791 |
-
grid: {
|
| 792 |
-
color: 'rgba(135, 191, 255, 0.1)',
|
| 793 |
-
drawBorder: false
|
| 794 |
-
},
|
| 795 |
-
ticks: {
|
| 796 |
-
color: '#87BFFF',
|
| 797 |
-
font: { size: 11 }
|
| 798 |
-
}
|
| 799 |
},
|
| 800 |
y: {
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
text: yLabel,
|
| 804 |
-
color: '#87BFFF',
|
| 805 |
-
font: { size: 12 }
|
| 806 |
-
},
|
| 807 |
-
grid: {
|
| 808 |
-
color: 'rgba(135, 191, 255, 0.1)',
|
| 809 |
-
drawBorder: false
|
| 810 |
-
},
|
| 811 |
-
ticks: {
|
| 812 |
-
color: '#87BFFF',
|
| 813 |
-
font: { size: 11 }
|
| 814 |
-
}
|
| 815 |
}
|
| 816 |
}
|
| 817 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
}
|
| 819 |
|
| 820 |
-
//
|
| 821 |
function exportResults() {
|
| 822 |
if (!simulationResults) return;
|
| 823 |
-
|
| 824 |
const data = {
|
| 825 |
timestamp: new Date().toISOString(),
|
| 826 |
parameters: simulationResults.parameters,
|
| 827 |
summary: simulationResults.summary,
|
| 828 |
runs: simulationResults.runs
|
| 829 |
};
|
| 830 |
-
|
| 831 |
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 832 |
const url = URL.createObjectURL(blob);
|
| 833 |
-
|
| 834 |
const a = document.createElement('a');
|
| 835 |
a.href = url;
|
| 836 |
-
a.download = `
|
| 837 |
document.body.appendChild(a);
|
| 838 |
a.click();
|
| 839 |
document.body.removeChild(a);
|
| 840 |
URL.revokeObjectURL(url);
|
| 841 |
}
|
| 842 |
|
| 843 |
-
// UI
|
| 844 |
function showProgress() {
|
| 845 |
-
document.
|
| 846 |
document.getElementById('progressFill').style.width = '0%';
|
| 847 |
}
|
| 848 |
|
| 849 |
function hideProgress() {
|
| 850 |
setTimeout(() => {
|
| 851 |
-
document.
|
| 852 |
}, 500);
|
| 853 |
}
|
| 854 |
|
| 855 |
function disableControls() {
|
| 856 |
const runBtn = document.getElementById('runBtn');
|
| 857 |
runBtn.disabled = true;
|
|
|
|
| 858 |
document.querySelector('.run-text').textContent = 'Running...';
|
| 859 |
-
document.querySelector('.run-spinner').style.display = 'inline-block';
|
| 860 |
}
|
| 861 |
|
| 862 |
function enableControls() {
|
| 863 |
const runBtn = document.getElementById('runBtn');
|
| 864 |
runBtn.disabled = false;
|
|
|
|
| 865 |
document.querySelector('.run-text').textContent = 'Run Simulation';
|
| 866 |
-
document.querySelector('.run-spinner').style.display = 'none';
|
| 867 |
-
}
|
| 868 |
-
|
| 869 |
-
// Utility functions
|
| 870 |
-
function sleep(ms) {
|
| 871 |
-
return new Promise(resolve => setTimeout(resolve, ms));
|
| 872 |
}
|
| 873 |
|
| 874 |
function formatPercentage(value) {
|
|
|
|
| 875 |
return `${(value * 100).toFixed(1)}%`;
|
| 876 |
}
|
| 877 |
|
| 878 |
-
|
| 879 |
-
if (value > 0.02) return 'positive';
|
| 880 |
-
if (value < -0.02) return 'negative';
|
| 881 |
-
return 'neutral';
|
| 882 |
-
}
|
| 883 |
-
|
| 884 |
function mean(arr) {
|
| 885 |
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
| 886 |
}
|
|
@@ -893,8 +559,7 @@ function median(arr) {
|
|
| 893 |
|
| 894 |
function standardDeviation(arr) {
|
| 895 |
const avg = mean(arr);
|
| 896 |
-
|
| 897 |
-
return Math.sqrt(mean(squareDiffs));
|
| 898 |
}
|
| 899 |
|
| 900 |
function percentile(arr, p) {
|
|
@@ -905,52 +570,3 @@ function percentile(arr, p) {
|
|
| 905 |
const weight = index % 1;
|
| 906 |
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
| 907 |
}
|
| 908 |
-
|
| 909 |
-
function createHistogram(data, bins) {
|
| 910 |
-
const min = Math.min(...data);
|
| 911 |
-
const max = Math.max(...data);
|
| 912 |
-
const binWidth = (max - min) / bins;
|
| 913 |
-
|
| 914 |
-
const histogram = new Array(bins).fill(0);
|
| 915 |
-
const labels = [];
|
| 916 |
-
|
| 917 |
-
for (let i = 0; i < bins; i++) {
|
| 918 |
-
const binStart = min + i * binWidth;
|
| 919 |
-
const binEnd = min + (i + 1) * binWidth;
|
| 920 |
-
labels.push(`${binStart.toFixed(1)}`);
|
| 921 |
-
|
| 922 |
-
for (const value of data) {
|
| 923 |
-
if (value >= binStart && (value < binEnd || i === bins - 1)) {
|
| 924 |
-
histogram[i]++;
|
| 925 |
-
}
|
| 926 |
-
}
|
| 927 |
-
}
|
| 928 |
-
|
| 929 |
-
return { labels, data: histogram };
|
| 930 |
-
}
|
| 931 |
-
|
| 932 |
-
// Simple RNG for reproducible results
|
| 933 |
-
class SimpleRNG {
|
| 934 |
-
constructor(seed) {
|
| 935 |
-
this.seed = seed % 2147483647;
|
| 936 |
-
if (this.seed <= 0) this.seed += 2147483646;
|
| 937 |
-
}
|
| 938 |
-
|
| 939 |
-
next() {
|
| 940 |
-
return this.seed = this.seed * 16807 % 2147483647;
|
| 941 |
-
}
|
| 942 |
-
|
| 943 |
-
nextFloat() {
|
| 944 |
-
return (this.next() - 1) / 2147483646;
|
| 945 |
-
}
|
| 946 |
-
|
| 947 |
-
nextGaussian() {
|
| 948 |
-
const u = 0.5 - this.nextFloat();
|
| 949 |
-
const v = this.nextFloat();
|
| 950 |
-
return Math.sqrt(-2.0 * Math.log(v)) * Math.cos(2.0 * Math.PI * u);
|
| 951 |
-
}
|
| 952 |
-
|
| 953 |
-
nextInt(min, max) {
|
| 954 |
-
return Math.floor(this.nextFloat() * (max - min + 1)) + min;
|
| 955 |
-
}
|
| 956 |
-
}
|
|
|
|
| 1 |
+
// Safe Choices - Simulation Controller
|
| 2 |
+
|
| 3 |
+
// ===== STATE =====
|
| 4 |
let currentSimType = 'single';
|
| 5 |
+
let currentView = 'simulation';
|
| 6 |
let simulationResults = null;
|
| 7 |
let charts = {};
|
| 8 |
|
| 9 |
+
// ===== INITIALIZATION =====
|
| 10 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 11 |
+
initializeApp();
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
function initializeApp() {
|
| 15 |
setupEventListeners();
|
| 16 |
updateEstimatedTime();
|
| 17 |
initializeEmptyCharts();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
|
|
|
| 20 |
function setupEventListeners() {
|
| 21 |
+
// Number input constraints
|
| 22 |
+
const numSimulations = document.getElementById('numSimulations');
|
| 23 |
+
numSimulations.addEventListener('input', () => {
|
| 24 |
+
numSimulations.value = Math.min(Math.max(parseInt(numSimulations.value) || 10, 10), 100);
|
| 25 |
updateEstimatedTime();
|
| 26 |
});
|
| 27 |
+
|
| 28 |
+
const numFunds = document.getElementById('numFunds');
|
| 29 |
+
numFunds.addEventListener('input', () => {
|
| 30 |
+
numFunds.value = Math.min(Math.max(parseInt(numFunds.value) || 2, 2), 10);
|
| 31 |
+
updateEstimatedTime();
|
| 32 |
});
|
| 33 |
+
|
| 34 |
+
// Target return custom input toggle
|
| 35 |
+
const targetReturn = document.getElementById('targetReturn');
|
| 36 |
+
const customTarget = document.getElementById('customTarget');
|
| 37 |
+
targetReturn.addEventListener('change', () => {
|
| 38 |
+
if (targetReturn.value === 'custom') {
|
| 39 |
+
customTarget.classList.add('visible');
|
| 40 |
+
customTarget.value = '15';
|
| 41 |
+
} else {
|
| 42 |
+
customTarget.classList.remove('visible');
|
| 43 |
+
}
|
| 44 |
});
|
| 45 |
+
|
| 46 |
+
// Validate inputs on change
|
| 47 |
+
document.querySelectorAll('.param-input').forEach(input => {
|
|
|
|
| 48 |
input.addEventListener('input', validateInputs);
|
| 49 |
});
|
| 50 |
}
|
| 51 |
|
| 52 |
+
// ===== VIEW SWITCHING =====
|
|
|
|
|
|
|
|
|
|
| 53 |
function selectView(view) {
|
| 54 |
currentView = view;
|
| 55 |
+
|
| 56 |
+
// Update toggle buttons
|
| 57 |
+
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
| 58 |
+
btn.classList.toggle('active', btn.dataset.view === view);
|
| 59 |
});
|
| 60 |
+
|
| 61 |
+
// Show/hide content
|
| 62 |
+
const simContent = document.getElementById('simulationContent');
|
| 63 |
+
const methContent = document.getElementById('methodologyContent');
|
| 64 |
+
|
|
|
|
| 65 |
if (view === 'simulation') {
|
| 66 |
+
simContent.style.display = 'flex';
|
| 67 |
+
methContent.style.display = 'none';
|
| 68 |
document.querySelectorAll('.simulation-view-only').forEach(el => {
|
| 69 |
el.classList.remove('hidden');
|
| 70 |
});
|
| 71 |
} else {
|
| 72 |
+
simContent.style.display = 'none';
|
| 73 |
+
methContent.style.display = 'block';
|
| 74 |
document.querySelectorAll('.simulation-view-only').forEach(el => {
|
| 75 |
el.classList.add('hidden');
|
| 76 |
});
|
| 77 |
}
|
| 78 |
}
|
| 79 |
|
| 80 |
+
// ===== SIMULATION TYPE SWITCHING =====
|
| 81 |
function selectSimulation(type) {
|
| 82 |
currentSimType = type;
|
| 83 |
+
|
| 84 |
+
// Update strategy cards
|
| 85 |
+
document.querySelectorAll('.strategy-card').forEach(card => {
|
| 86 |
+
card.classList.toggle('active', card.dataset.sim === type);
|
| 87 |
});
|
| 88 |
+
|
| 89 |
+
// Show/hide conditional parameters
|
|
|
|
| 90 |
document.querySelectorAll('.threshold-only').forEach(el => {
|
| 91 |
el.classList.toggle('visible', type === 'threshold');
|
| 92 |
});
|
| 93 |
+
|
| 94 |
document.querySelectorAll('.multi-only').forEach(el => {
|
| 95 |
el.classList.toggle('visible', type === 'multi');
|
| 96 |
});
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
updateEstimatedTime();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
+
// ===== VALIDATION =====
|
| 102 |
function validateInputs() {
|
| 103 |
const errors = [];
|
| 104 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
const minProb7d = parseFloat(document.getElementById('minProb7d').value);
|
| 106 |
const minProbCurrent = parseFloat(document.getElementById('minProbCurrent').value);
|
| 107 |
+
|
| 108 |
if (minProb7d < 50 || minProb7d > 99) {
|
| 109 |
errors.push('7-day probability must be between 50% and 99%');
|
| 110 |
}
|
| 111 |
if (minProbCurrent < 50 || minProbCurrent > 99) {
|
| 112 |
errors.push('Current probability must be between 50% and 99%');
|
| 113 |
}
|
| 114 |
+
|
|
|
|
| 115 |
const runBtn = document.getElementById('runBtn');
|
| 116 |
runBtn.disabled = errors.length > 0;
|
| 117 |
+
|
| 118 |
return errors.length === 0;
|
| 119 |
}
|
| 120 |
|
| 121 |
+
// ===== TIME ESTIMATION =====
|
| 122 |
+
function updateEstimatedTime() {
|
| 123 |
+
const numSims = parseInt(document.getElementById('numSimulations').value) || 100;
|
| 124 |
+
const numFunds = currentSimType === 'multi' ? (parseInt(document.getElementById('numFunds').value) || 5) : 1;
|
| 125 |
+
|
| 126 |
+
let baseTime = numSims * 0.05;
|
| 127 |
+
if (currentSimType === 'multi') {
|
| 128 |
+
baseTime *= Math.sqrt(numFunds);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
const seconds = Math.max(2, Math.ceil(baseTime));
|
| 132 |
+
const timeText = seconds < 60 ? `~${seconds}s` : `~${Math.ceil(seconds / 60)}min`;
|
| 133 |
+
|
| 134 |
+
document.getElementById('estimatedTime').textContent = timeText;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ===== SIMULATION EXECUTION =====
|
| 138 |
async function runSimulation() {
|
| 139 |
if (!validateInputs()) return;
|
| 140 |
+
|
|
|
|
| 141 |
const params = getSimulationParameters();
|
| 142 |
+
|
|
|
|
| 143 |
showProgress();
|
| 144 |
disableControls();
|
| 145 |
+
|
| 146 |
try {
|
|
|
|
| 147 |
const results = await callSimulationAPI(params);
|
|
|
|
|
|
|
| 148 |
simulationResults = results;
|
| 149 |
displayResults(results);
|
|
|
|
| 150 |
} catch (error) {
|
| 151 |
console.error('Simulation error:', error);
|
| 152 |
+
alert('Simulation failed: ' + (error.message || 'Unknown error'));
|
| 153 |
} finally {
|
| 154 |
hideProgress();
|
| 155 |
enableControls();
|
| 156 |
}
|
| 157 |
}
|
| 158 |
|
| 159 |
+
function getSimulationParameters() {
|
| 160 |
+
const params = {
|
| 161 |
+
simType: currentSimType,
|
| 162 |
+
startingCapital: parseFloat(document.getElementById('startingCapital').value),
|
| 163 |
+
numSimulations: parseInt(document.getElementById('numSimulations').value),
|
| 164 |
+
startDate: document.getElementById('startDate').value,
|
| 165 |
+
maxDuration: parseInt(document.getElementById('maxDuration').value),
|
| 166 |
+
minProb7d: parseFloat(document.getElementById('minProb7d').value) / 100,
|
| 167 |
+
minProbCurrent: parseFloat(document.getElementById('minProbCurrent').value) / 100,
|
| 168 |
+
daysBefore: parseInt(document.getElementById('daysBefore').value),
|
| 169 |
+
investmentProbability: parseFloat(document.getElementById('investmentProbability').value)
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
if (currentSimType === 'threshold') {
|
| 173 |
+
const targetSelect = document.getElementById('targetReturn').value;
|
| 174 |
+
if (targetSelect === 'custom') {
|
| 175 |
+
params.targetReturn = parseFloat(document.getElementById('customTarget').value) / 100;
|
| 176 |
+
} else {
|
| 177 |
+
params.targetReturn = parseFloat(targetSelect) / 100;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
if (currentSimType === 'multi') {
|
| 182 |
+
params.numFunds = parseInt(document.getElementById('numFunds').value);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
return params;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
async function callSimulationAPI(params) {
|
| 189 |
const progressFill = document.getElementById('progressFill');
|
| 190 |
const progressPercent = document.getElementById('progressPercent');
|
| 191 |
const progressText = document.getElementById('progressText');
|
| 192 |
+
|
| 193 |
+
progressText.textContent = 'Connecting to server...';
|
| 194 |
progressFill.style.width = '10%';
|
| 195 |
progressPercent.textContent = '10%';
|
| 196 |
+
|
|
|
|
| 197 |
const requestBody = {
|
| 198 |
simType: params.simType,
|
| 199 |
startingCapital: params.startingCapital,
|
|
|
|
| 205 |
daysBefore: params.daysBefore,
|
| 206 |
investmentProbability: params.investmentProbability
|
| 207 |
};
|
| 208 |
+
|
| 209 |
+
if (params.targetReturn !== undefined) {
|
| 210 |
requestBody.targetReturn = params.targetReturn;
|
| 211 |
}
|
| 212 |
+
if (params.numFunds !== undefined) {
|
|
|
|
| 213 |
requestBody.numFunds = params.numFunds;
|
| 214 |
}
|
| 215 |
+
|
| 216 |
+
progressText.textContent = 'Running simulation...';
|
| 217 |
+
progressFill.style.width = '30%';
|
| 218 |
+
progressPercent.textContent = '30%';
|
| 219 |
+
|
| 220 |
const response = await fetch('/api/simulate', {
|
| 221 |
method: 'POST',
|
| 222 |
+
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
|
| 223 |
body: JSON.stringify(requestBody)
|
| 224 |
});
|
| 225 |
+
|
| 226 |
if (!response.ok) {
|
| 227 |
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
| 228 |
+
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
| 229 |
}
|
| 230 |
+
|
| 231 |
progressText.textContent = 'Processing results...';
|
| 232 |
+
progressFill.style.width = '80%';
|
| 233 |
+
progressPercent.textContent = '80%';
|
| 234 |
+
|
| 235 |
const results = await response.json();
|
| 236 |
+
|
| 237 |
progressFill.style.width = '100%';
|
| 238 |
progressPercent.textContent = '100%';
|
| 239 |
progressText.textContent = 'Complete!';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
return results;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
+
// ===== RESULTS DISPLAY =====
|
| 245 |
+
function displayResults(results) {
|
| 246 |
+
const { summary, parameters } = results;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
+
// Show results section
|
| 249 |
+
const resultsSection = document.getElementById('resultsSection');
|
| 250 |
+
resultsSection.classList.add('visible');
|
| 251 |
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
+
// Primary metrics
|
| 254 |
+
updateMetric('avgReturn', summary.avgReturn, true);
|
| 255 |
+
updateMetric('medianReturn', summary.medianReturn, true);
|
| 256 |
+
updateMetric('successRate', summary.positiveReturnRate);
|
| 257 |
+
updateMetric('bustRate', summary.bustRate, false, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
+
// Risk metrics
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
document.getElementById('volatility').textContent = formatPercentage(summary.returnVolatility);
|
| 261 |
document.getElementById('maxDrawdown').textContent = formatPercentage(summary.maxDrawdown);
|
| 262 |
+
document.getElementById('percentile5').textContent = formatPercentage(summary.return5th || -1);
|
| 263 |
+
document.getElementById('percentile95').textContent = formatPercentage(summary.return95th || 0);
|
| 264 |
+
|
| 265 |
+
// Type-specific stats
|
| 266 |
if (parameters.simType === 'multi') {
|
| 267 |
document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
|
| 268 |
el.classList.add('visible');
|
| 269 |
});
|
| 270 |
+
document.getElementById('avgSurvivingFunds').textContent =
|
| 271 |
+
`${summary.avgSurvivingFunds?.toFixed(1) || '--'} / ${parameters.numFunds}`;
|
| 272 |
+
document.getElementById('survivorshipRate').textContent = formatPercentage(summary.survivorshipRate || 0);
|
| 273 |
+
document.getElementById('diversificationBenefit').textContent =
|
| 274 |
+
summary.returnVolatility < 0.3 ? 'Positive' : 'Limited';
|
|
|
|
|
|
|
|
|
|
| 275 |
} else {
|
| 276 |
document.querySelectorAll('.multi-stats, .multi-chart').forEach(el => {
|
| 277 |
el.classList.remove('visible');
|
| 278 |
});
|
| 279 |
}
|
| 280 |
+
|
| 281 |
if (parameters.simType === 'threshold') {
|
| 282 |
document.querySelectorAll('.threshold-stats').forEach(el => {
|
| 283 |
el.classList.add('visible');
|
| 284 |
});
|
|
|
|
| 285 |
document.getElementById('targetReached').textContent = formatPercentage(summary.targetReachedRate || 0);
|
| 286 |
+
document.getElementById('avgTimeToTarget').textContent =
|
| 287 |
summary.avgTimeToTarget ? `${Math.round(summary.avgTimeToTarget)} days` : 'N/A';
|
| 288 |
+
document.getElementById('vsNeverStop').textContent =
|
| 289 |
+
(summary.targetReachedRate || 0) > 0.5 ? 'Better' : 'Similar';
|
| 290 |
} else {
|
| 291 |
document.querySelectorAll('.threshold-stats').forEach(el => {
|
| 292 |
el.classList.remove('visible');
|
| 293 |
});
|
| 294 |
}
|
| 295 |
+
|
| 296 |
// Generate charts
|
| 297 |
generateCharts(results);
|
| 298 |
}
|
| 299 |
|
| 300 |
+
function updateMetric(id, value, showSign = false, isRisk = false) {
|
| 301 |
+
const el = document.getElementById(id);
|
| 302 |
+
el.textContent = formatPercentage(value);
|
| 303 |
+
|
| 304 |
+
// Add color classes
|
| 305 |
+
el.classList.remove('positive', 'negative', 'high-risk');
|
| 306 |
+
if (isRisk) {
|
| 307 |
+
if (value > 0.1) el.classList.add('high-risk');
|
| 308 |
+
} else if (showSign) {
|
| 309 |
+
if (value > 0.02) el.classList.add('positive');
|
| 310 |
+
else if (value < -0.02) el.classList.add('negative');
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// ===== CHARTS =====
|
| 315 |
function initializeEmptyCharts() {
|
| 316 |
+
charts.return = createEmptyChart('returnChart', 'bar');
|
| 317 |
+
charts.capital = createEmptyChart('capitalChart', 'line');
|
| 318 |
+
charts.survivorship = createEmptyChart('survivorshipChart', 'bar');
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
function createEmptyChart(canvasId, type) {
|
| 322 |
+
const ctx = document.getElementById(canvasId).getContext('2d');
|
| 323 |
+
return new Chart(ctx, {
|
| 324 |
+
type: type,
|
| 325 |
+
data: { labels: [], datasets: [] },
|
| 326 |
+
options: getChartOptions(type)
|
| 327 |
+
});
|
| 328 |
}
|
| 329 |
|
|
|
|
| 330 |
function generateCharts(results) {
|
| 331 |
+
const { runs, parameters } = results;
|
| 332 |
+
|
| 333 |
// Destroy existing charts
|
| 334 |
+
Object.values(charts).forEach(chart => chart?.destroy());
|
|
|
|
|
|
|
| 335 |
charts = {};
|
| 336 |
+
|
| 337 |
+
// Return distribution
|
| 338 |
charts.return = createReturnDistributionChart(runs);
|
| 339 |
+
|
| 340 |
+
// Capital evolution
|
| 341 |
charts.capital = createCapitalEvolutionChart(runs);
|
| 342 |
+
|
| 343 |
+
// Survivorship (multi-fund only)
|
| 344 |
if (parameters.simType === 'multi') {
|
| 345 |
charts.survivorship = createSurvivorshipChart(runs, parameters.numFunds);
|
| 346 |
} else {
|
| 347 |
+
charts.survivorship = createEmptyChart('survivorshipChart', 'bar');
|
| 348 |
}
|
| 349 |
}
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
function createReturnDistributionChart(runs) {
|
| 352 |
const ctx = document.getElementById('returnChart').getContext('2d');
|
| 353 |
const returns = runs.map(r => r.totalReturn * 100);
|
| 354 |
+
|
|
|
|
| 355 |
const minReturn = Math.min(...returns);
|
| 356 |
const maxReturn = Math.max(...returns);
|
|
|
|
|
|
|
| 357 |
const binStart = Math.floor(minReturn / 10) * 10;
|
| 358 |
+
const binEnd = Math.min(Math.ceil(maxReturn / 10) * 10, 200);
|
| 359 |
+
|
|
|
|
| 360 |
const bins = [];
|
| 361 |
+
for (let i = binStart; i <= binEnd; i += 5) bins.push(i);
|
| 362 |
+
|
|
|
|
|
|
|
|
|
|
| 363 |
const binCounts = new Array(bins.length).fill(0);
|
| 364 |
returns.forEach(ret => {
|
| 365 |
for (let i = 0; i < bins.length - 1; i++) {
|
|
|
|
| 368 |
break;
|
| 369 |
}
|
| 370 |
}
|
| 371 |
+
if (ret >= bins[bins.length - 1]) binCounts[bins.length - 1]++;
|
|
|
|
|
|
|
|
|
|
| 372 |
});
|
| 373 |
+
|
| 374 |
return new Chart(ctx, {
|
| 375 |
type: 'bar',
|
| 376 |
data: {
|
|
|
|
| 378 |
datasets: [{
|
| 379 |
label: 'Frequency',
|
| 380 |
data: binCounts,
|
| 381 |
+
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
| 382 |
+
borderColor: 'rgba(59, 130, 246, 1)',
|
| 383 |
+
borderWidth: 1,
|
| 384 |
+
borderRadius: 4
|
| 385 |
}]
|
| 386 |
},
|
| 387 |
+
options: getChartOptions('bar')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
});
|
| 389 |
}
|
| 390 |
|
|
|
|
| 391 |
function createCapitalEvolutionChart(runs) {
|
| 392 |
const ctx = document.getElementById('capitalChart').getContext('2d');
|
| 393 |
+
|
|
|
|
| 394 |
const colors = [
|
| 395 |
+
{ border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
| 396 |
+
{ border: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
| 397 |
+
{ border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
| 398 |
+
{ border: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
| 399 |
+
{ border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }
|
| 400 |
];
|
| 401 |
+
|
|
|
|
| 402 |
const sampleRuns = runs.slice(0, 5);
|
| 403 |
const datasets = sampleRuns.map((run, i) => {
|
|
|
|
| 404 |
let data = [];
|
| 405 |
+
if (run.capitalHistory?.length > 0) {
|
| 406 |
+
data = run.capitalHistory.map((item, idx) => ({
|
| 407 |
+
x: typeof item === 'object' ? item.day : idx,
|
| 408 |
+
y: typeof item === 'object' ? item.capital : item
|
| 409 |
+
}));
|
|
|
|
|
|
|
| 410 |
} else {
|
| 411 |
+
data = [{ x: 0, y: 10000 }, { x: 1, y: run.finalCapital }];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
}
|
| 413 |
+
|
| 414 |
return {
|
| 415 |
label: `Run ${i + 1}`,
|
| 416 |
data: data,
|
| 417 |
+
borderColor: colors[i].border,
|
| 418 |
+
backgroundColor: colors[i].bg,
|
| 419 |
borderWidth: 2,
|
| 420 |
fill: false,
|
| 421 |
+
tension: 0.3,
|
| 422 |
+
pointRadius: 0
|
| 423 |
};
|
| 424 |
});
|
| 425 |
+
|
| 426 |
return new Chart(ctx, {
|
| 427 |
type: 'line',
|
| 428 |
data: { datasets },
|
| 429 |
+
options: getChartOptions('line')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
});
|
| 431 |
}
|
| 432 |
|
|
|
|
| 433 |
function createSurvivorshipChart(runs, numFunds) {
|
| 434 |
const ctx = document.getElementById('survivorshipChart').getContext('2d');
|
| 435 |
const survivingCounts = runs.map(r => r.survivingFunds);
|
| 436 |
+
|
|
|
|
| 437 |
const labels = [];
|
| 438 |
const data = [];
|
| 439 |
for (let i = 0; i <= numFunds; i++) {
|
| 440 |
labels.push(i.toString());
|
| 441 |
data.push(survivingCounts.filter(c => c === i).length);
|
| 442 |
}
|
| 443 |
+
|
| 444 |
return new Chart(ctx, {
|
| 445 |
type: 'bar',
|
| 446 |
data: {
|
|
|
|
| 448 |
datasets: [{
|
| 449 |
label: 'Frequency',
|
| 450 |
data: data,
|
| 451 |
+
backgroundColor: 'rgba(139, 92, 246, 0.6)',
|
| 452 |
+
borderColor: 'rgba(139, 92, 246, 1)',
|
| 453 |
+
borderWidth: 1,
|
| 454 |
+
borderRadius: 4
|
| 455 |
}]
|
| 456 |
},
|
| 457 |
+
options: getChartOptions('bar')
|
| 458 |
});
|
| 459 |
}
|
| 460 |
|
| 461 |
+
function getChartOptions(type) {
|
| 462 |
+
const baseOptions = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
responsive: true,
|
| 464 |
maintainAspectRatio: false,
|
| 465 |
plugins: {
|
| 466 |
+
legend: {
|
| 467 |
+
display: type === 'line',
|
| 468 |
labels: {
|
| 469 |
+
color: '#9ca3af',
|
| 470 |
font: { size: 11 },
|
| 471 |
usePointStyle: true,
|
| 472 |
+
padding: 16
|
| 473 |
}
|
| 474 |
}
|
| 475 |
},
|
| 476 |
scales: {
|
| 477 |
x: {
|
| 478 |
+
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
| 479 |
+
ticks: { color: '#6b7280', font: { size: 10 } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
},
|
| 481 |
y: {
|
| 482 |
+
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
| 483 |
+
ticks: { color: '#6b7280', font: { size: 10 } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
}
|
| 485 |
}
|
| 486 |
};
|
| 487 |
+
|
| 488 |
+
if (type === 'line') {
|
| 489 |
+
baseOptions.scales.x.type = 'linear';
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
return baseOptions;
|
| 493 |
}
|
| 494 |
|
| 495 |
+
// ===== EXPORT =====
|
| 496 |
function exportResults() {
|
| 497 |
if (!simulationResults) return;
|
| 498 |
+
|
| 499 |
const data = {
|
| 500 |
timestamp: new Date().toISOString(),
|
| 501 |
parameters: simulationResults.parameters,
|
| 502 |
summary: simulationResults.summary,
|
| 503 |
runs: simulationResults.runs
|
| 504 |
};
|
| 505 |
+
|
| 506 |
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 507 |
const url = URL.createObjectURL(blob);
|
| 508 |
+
|
| 509 |
const a = document.createElement('a');
|
| 510 |
a.href = url;
|
| 511 |
+
a.download = `safe_choices_${currentSimType}_${Date.now()}.json`;
|
| 512 |
document.body.appendChild(a);
|
| 513 |
a.click();
|
| 514 |
document.body.removeChild(a);
|
| 515 |
URL.revokeObjectURL(url);
|
| 516 |
}
|
| 517 |
|
| 518 |
+
// ===== UI HELPERS =====
|
| 519 |
function showProgress() {
|
| 520 |
+
document.getElementById('progressPanel').classList.add('visible');
|
| 521 |
document.getElementById('progressFill').style.width = '0%';
|
| 522 |
}
|
| 523 |
|
| 524 |
function hideProgress() {
|
| 525 |
setTimeout(() => {
|
| 526 |
+
document.getElementById('progressPanel').classList.remove('visible');
|
| 527 |
}, 500);
|
| 528 |
}
|
| 529 |
|
| 530 |
function disableControls() {
|
| 531 |
const runBtn = document.getElementById('runBtn');
|
| 532 |
runBtn.disabled = true;
|
| 533 |
+
runBtn.classList.add('running');
|
| 534 |
document.querySelector('.run-text').textContent = 'Running...';
|
|
|
|
| 535 |
}
|
| 536 |
|
| 537 |
function enableControls() {
|
| 538 |
const runBtn = document.getElementById('runBtn');
|
| 539 |
runBtn.disabled = false;
|
| 540 |
+
runBtn.classList.remove('running');
|
| 541 |
document.querySelector('.run-text').textContent = 'Run Simulation';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
}
|
| 543 |
|
| 544 |
function formatPercentage(value) {
|
| 545 |
+
if (value === null || value === undefined || isNaN(value)) return '--';
|
| 546 |
return `${(value * 100).toFixed(1)}%`;
|
| 547 |
}
|
| 548 |
|
| 549 |
+
// ===== UTILITY FUNCTIONS =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
function mean(arr) {
|
| 551 |
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
| 552 |
}
|
|
|
|
| 559 |
|
| 560 |
function standardDeviation(arr) {
|
| 561 |
const avg = mean(arr);
|
| 562 |
+
return Math.sqrt(arr.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / arr.length);
|
|
|
|
| 563 |
}
|
| 564 |
|
| 565 |
function percentile(arr, p) {
|
|
|
|
| 570 |
const weight = index % 1;
|
| 571 |
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
| 572 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/styles.css
CHANGED
|
@@ -1,431 +1,655 @@
|
|
| 1 |
-
/*
|
| 2 |
|
| 3 |
:root {
|
| 4 |
-
/*
|
| 5 |
-
--bg-
|
| 6 |
-
--bg-
|
| 7 |
-
--bg-
|
| 8 |
-
--
|
| 9 |
-
|
| 10 |
-
--
|
| 11 |
-
--
|
| 12 |
-
--
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
--
|
| 17 |
-
--
|
| 18 |
-
|
| 19 |
-
/* Status Colors (minimal use) */
|
| 20 |
--success: #10b981;
|
| 21 |
-
--
|
|
|
|
|
|
|
| 22 |
--warning: #f59e0b;
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
--
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
-
/* Reset
|
| 31 |
-
* {
|
| 32 |
margin: 0;
|
| 33 |
padding: 0;
|
| 34 |
box-sizing: border-box;
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
body {
|
| 38 |
-
font-family: var(--font
|
| 39 |
-
background
|
| 40 |
color: var(--text-primary);
|
| 41 |
line-height: 1.6;
|
| 42 |
min-height: 100vh;
|
| 43 |
-webkit-font-smoothing: antialiased;
|
| 44 |
-
-moz-osx-font-smoothing: grayscale;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
/*
|
| 48 |
-
.
|
| 49 |
-
background
|
| 50 |
-
border-bottom: 1px solid var(--border
|
| 51 |
position: sticky;
|
| 52 |
top: 0;
|
| 53 |
z-index: 100;
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
-
.
|
| 57 |
-
max-width:
|
| 58 |
margin: 0 auto;
|
| 59 |
-
padding: 0
|
|
|
|
| 60 |
display: flex;
|
| 61 |
align-items: center;
|
| 62 |
justify-content: space-between;
|
| 63 |
-
height: 64px;
|
| 64 |
}
|
| 65 |
|
| 66 |
-
.
|
| 67 |
display: flex;
|
| 68 |
align-items: center;
|
| 69 |
gap: 12px;
|
| 70 |
}
|
| 71 |
|
| 72 |
-
.
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
-
.
|
| 78 |
font-size: 18px;
|
| 79 |
-
font-weight:
|
| 80 |
-
|
| 81 |
-
letter-spacing: -0.01em;
|
| 82 |
}
|
| 83 |
|
| 84 |
-
.
|
| 85 |
-
font-size:
|
| 86 |
-
color: var(--text-
|
| 87 |
font-weight: 400;
|
| 88 |
}
|
| 89 |
|
| 90 |
-
.
|
| 91 |
display: flex;
|
| 92 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
-
.
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
background: var(--
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
border: 1px solid var(--border-color);
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
margin: 0 auto;
|
| 108 |
-
padding:
|
| 109 |
display: grid;
|
| 110 |
-
grid-template-columns:
|
| 111 |
-
gap:
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
-
/*
|
| 115 |
.sidebar {
|
| 116 |
display: flex;
|
| 117 |
flex-direction: column;
|
| 118 |
-
gap:
|
|
|
|
|
|
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
-
.sidebar-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
font-size: 11px;
|
| 123 |
font-weight: 600;
|
| 124 |
-
color: var(--text-secondary);
|
| 125 |
text-transform: uppercase;
|
| 126 |
-
letter-spacing: 0.
|
| 127 |
-
|
| 128 |
-
padding:
|
| 129 |
}
|
| 130 |
|
| 131 |
-
|
|
|
|
| 132 |
display: flex;
|
| 133 |
flex-direction: column;
|
| 134 |
-
gap:
|
| 135 |
}
|
| 136 |
|
| 137 |
-
.
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
background:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
cursor: pointer;
|
| 144 |
-
transition: all 0.
|
| 145 |
text-align: left;
|
| 146 |
-
margin: 0;
|
| 147 |
}
|
| 148 |
|
| 149 |
-
.
|
| 150 |
-
|
| 151 |
-
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
-
.
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
border-left: 3px solid var(--accent-dark);
|
| 158 |
}
|
| 159 |
|
| 160 |
-
.
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
font-weight: 600;
|
| 163 |
color: var(--text-primary);
|
| 164 |
-
margin-bottom: 3px;
|
| 165 |
}
|
| 166 |
|
| 167 |
-
.
|
| 168 |
-
font-size:
|
| 169 |
-
color: var(--text-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
display: flex;
|
| 176 |
flex-direction: column;
|
| 177 |
-
gap:
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
|
| 181 |
-
/*
|
| 182 |
-
.
|
| 183 |
-
background: var(--bg-
|
| 184 |
-
border
|
| 185 |
-
|
| 186 |
-
|
| 187 |
}
|
| 188 |
|
| 189 |
-
.
|
| 190 |
-
margin-bottom:
|
| 191 |
}
|
| 192 |
|
| 193 |
-
.
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
| 195 |
font-weight: 600;
|
| 196 |
-
margin-bottom: 6px;
|
| 197 |
color: var(--text-primary);
|
| 198 |
-
letter-spacing: -0.01em;
|
| 199 |
}
|
| 200 |
|
| 201 |
-
.
|
| 202 |
-
|
| 203 |
-
color: var(--text-secondary);
|
| 204 |
-
font-weight: 400;
|
| 205 |
}
|
| 206 |
|
| 207 |
-
|
|
|
|
| 208 |
display: grid;
|
| 209 |
-
grid-template-columns: repeat(auto-
|
| 210 |
-
gap:
|
| 211 |
-
margin-bottom:
|
| 212 |
}
|
| 213 |
|
| 214 |
-
.
|
| 215 |
display: flex;
|
| 216 |
flex-direction: column;
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
-
.
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
|
| 223 |
-
.
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
-
.
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
color: var(--text-primary);
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
font-family: var(--font
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
-
.
|
|
|
|
| 243 |
border-color: var(--accent);
|
| 244 |
-
|
| 245 |
-
outline-offset: 0;
|
| 246 |
}
|
| 247 |
|
| 248 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
font-size: 12px;
|
| 250 |
-
color: var(--text-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
-
/*
|
| 256 |
-
.run-
|
| 257 |
display: flex;
|
| 258 |
align-items: center;
|
| 259 |
justify-content: center;
|
| 260 |
-
gap:
|
| 261 |
-
padding-top:
|
| 262 |
-
border-top: 1px solid var(--border
|
| 263 |
-
margin-top: calc(var(--spacing-unit) * 0.5);
|
| 264 |
}
|
| 265 |
|
| 266 |
-
.run-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
padding: 12px 28px;
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
| 272 |
font-size: 14px;
|
| 273 |
font-weight: 600;
|
|
|
|
| 274 |
cursor: pointer;
|
| 275 |
-
transition:
|
| 276 |
-
|
| 277 |
-
align-items: center;
|
| 278 |
-
gap: 8px;
|
| 279 |
-
font-family: var(--font-body);
|
| 280 |
}
|
| 281 |
|
| 282 |
-
.run-
|
| 283 |
-
|
|
|
|
| 284 |
}
|
| 285 |
|
| 286 |
-
.run-
|
| 287 |
-
|
| 288 |
-
color: var(--text-secondary);
|
| 289 |
cursor: not-allowed;
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
.run-spinner {
|
|
|
|
| 294 |
width: 18px;
|
| 295 |
height: 18px;
|
| 296 |
-
border: 2px solid
|
| 297 |
-
border-top:
|
| 298 |
border-radius: 50%;
|
| 299 |
animation: spin 0.8s linear infinite;
|
| 300 |
}
|
| 301 |
|
| 302 |
@keyframes spin {
|
| 303 |
-
|
| 304 |
-
100% { transform: rotate(360deg); }
|
| 305 |
}
|
| 306 |
|
| 307 |
-
.run-
|
| 308 |
-
font-size:
|
| 309 |
-
color: var(--text-
|
| 310 |
}
|
| 311 |
|
| 312 |
-
/*
|
| 313 |
-
.progress-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
padding: calc(var(--spacing-unit) * 2);
|
| 317 |
-
border: 1px solid var(--border-color);
|
| 318 |
}
|
| 319 |
|
| 320 |
-
.progress-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
margin: 0 auto;
|
| 323 |
}
|
| 324 |
|
| 325 |
-
.progress-
|
| 326 |
display: flex;
|
| 327 |
justify-content: space-between;
|
| 328 |
align-items: center;
|
| 329 |
margin-bottom: 10px;
|
| 330 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
| 331 |
color: var(--text-primary);
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
height: 6px;
|
| 337 |
-
background: var(--bg-
|
| 338 |
border-radius: 3px;
|
| 339 |
overflow: hidden;
|
| 340 |
}
|
| 341 |
|
| 342 |
.progress-fill {
|
| 343 |
height: 100%;
|
| 344 |
-
background: var(--accent);
|
| 345 |
-
border-radius: 3px;
|
| 346 |
-
transition: width 0.2s ease;
|
| 347 |
width: 0%;
|
|
|
|
|
|
|
|
|
|
| 348 |
}
|
| 349 |
|
| 350 |
-
/*
|
| 351 |
-
.results
|
| 352 |
-
display:
|
| 353 |
flex-direction: column;
|
| 354 |
-
gap:
|
| 355 |
}
|
| 356 |
|
| 357 |
-
.results
|
| 358 |
display: flex;
|
| 359 |
-
justify-content: space-between;
|
| 360 |
-
align-items: center;
|
| 361 |
-
background: var(--bg-secondary);
|
| 362 |
-
padding: calc(var(--spacing-unit) * 1.75) calc(var(--spacing-unit) * 2);
|
| 363 |
-
border-radius: var(--radius-card);
|
| 364 |
-
border: 1px solid var(--border-color);
|
| 365 |
}
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
}
|
| 373 |
|
| 374 |
-
.
|
| 375 |
-
background: var(--bg-
|
| 376 |
-
|
| 377 |
-
border:
|
| 378 |
-
padding:
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
font-weight: 500;
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
|
|
|
| 385 |
}
|
| 386 |
|
| 387 |
-
.
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
display: grid;
|
| 395 |
-
grid-template-columns: repeat(2, 1fr);
|
| 396 |
-
gap: calc(var(--spacing-unit) * 2);
|
| 397 |
-
margin-bottom: calc(var(--spacing-unit) * 1.5);
|
| 398 |
}
|
| 399 |
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
grid-template-columns: 1fr;
|
| 403 |
-
}
|
| 404 |
}
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
min-height: 150px;
|
| 412 |
}
|
| 413 |
|
| 414 |
-
.stat-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
| 416 |
}
|
| 417 |
|
| 418 |
-
.stat-
|
| 419 |
-
font-size:
|
| 420 |
font-weight: 600;
|
| 421 |
color: var(--text-primary);
|
| 422 |
-
|
|
|
|
|
|
|
| 423 |
}
|
| 424 |
|
| 425 |
-
.stat-
|
| 426 |
display: flex;
|
| 427 |
flex-direction: column;
|
| 428 |
-
gap:
|
| 429 |
}
|
| 430 |
|
| 431 |
.stat-item {
|
|
@@ -434,329 +658,358 @@ body {
|
|
| 434 |
align-items: center;
|
| 435 |
}
|
| 436 |
|
| 437 |
-
.stat-label {
|
| 438 |
font-size: 13px;
|
| 439 |
color: var(--text-secondary);
|
| 440 |
-
font-weight: 400;
|
| 441 |
}
|
| 442 |
|
| 443 |
-
.stat-value {
|
| 444 |
-
font-size:
|
| 445 |
font-weight: 600;
|
| 446 |
color: var(--text-primary);
|
| 447 |
-
letter-spacing: -0.01em;
|
| 448 |
}
|
| 449 |
|
| 450 |
-
.
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
.stat-value.negative {
|
| 455 |
-
color: var(--error);
|
| 456 |
}
|
| 457 |
|
| 458 |
-
.
|
| 459 |
-
|
|
|
|
| 460 |
}
|
| 461 |
|
| 462 |
-
/* Charts
|
| 463 |
-
.charts-
|
| 464 |
display: grid;
|
| 465 |
grid-template-columns: repeat(2, 1fr);
|
| 466 |
-
gap:
|
| 467 |
-
margin-top: calc(var(--spacing-unit) * 2);
|
| 468 |
}
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
.chart-card {
|
| 477 |
-
background: var(--bg-secondary);
|
| 478 |
-
border-radius: var(--radius-card);
|
| 479 |
-
padding: calc(var(--spacing-unit) * 1.5);
|
| 480 |
-
border: 1px solid var(--border-color);
|
| 481 |
}
|
| 482 |
|
| 483 |
.chart-header {
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
| 485 |
}
|
| 486 |
|
| 487 |
.chart-header h3 {
|
| 488 |
-
font-size:
|
| 489 |
font-weight: 600;
|
| 490 |
color: var(--text-primary);
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
}
|
| 493 |
|
| 494 |
.chart-container {
|
|
|
|
| 495 |
position: relative;
|
| 496 |
-
height: 300px;
|
| 497 |
-
width: 100%;
|
| 498 |
-
padding: 8px 0;
|
| 499 |
}
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
grid-template-columns: 1fr;
|
| 505 |
-
gap: calc(var(--spacing-unit) * 1.5);
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
.sidebar {
|
| 509 |
-
order: 2;
|
| 510 |
-
}
|
| 511 |
-
|
| 512 |
-
.content-area {
|
| 513 |
-
order: 1;
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
.simulation-tabs {
|
| 517 |
-
flex-direction: row;
|
| 518 |
-
flex-wrap: wrap;
|
| 519 |
-
}
|
| 520 |
-
|
| 521 |
-
.sim-tab {
|
| 522 |
-
flex: 1;
|
| 523 |
-
min-width: 200px;
|
| 524 |
-
}
|
| 525 |
}
|
| 526 |
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
padding: 0 var(--spacing-unit);
|
| 530 |
-
height: 56px;
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
.app-title {
|
| 534 |
-
font-size: 16px;
|
| 535 |
-
}
|
| 536 |
-
|
| 537 |
-
.subtitle {
|
| 538 |
-
font-size: 12px;
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
.main-container {
|
| 542 |
-
padding: var(--spacing-unit);
|
| 543 |
-
gap: var(--spacing-unit);
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
.controls-section {
|
| 547 |
-
padding: calc(var(--spacing-unit) * 1.5);
|
| 548 |
-
}
|
| 549 |
-
|
| 550 |
-
.controls-grid {
|
| 551 |
-
grid-template-columns: 1fr;
|
| 552 |
-
gap: var(--spacing-unit);
|
| 553 |
-
}
|
| 554 |
-
|
| 555 |
-
.charts-grid {
|
| 556 |
-
grid-template-columns: 1fr;
|
| 557 |
-
gap: calc(var(--spacing-unit) * 1.5);
|
| 558 |
-
}
|
| 559 |
-
|
| 560 |
-
.chart-card,
|
| 561 |
-
.stat-card {
|
| 562 |
-
padding: calc(var(--spacing-unit) * 1.5);
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
.results-header {
|
| 566 |
-
flex-direction: column;
|
| 567 |
-
gap: var(--spacing-unit);
|
| 568 |
-
align-items: stretch;
|
| 569 |
-
padding: calc(var(--spacing-unit) * 1.5);
|
| 570 |
-
}
|
| 571 |
}
|
| 572 |
|
| 573 |
-
/*
|
| 574 |
-
.
|
| 575 |
-
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
-
.
|
| 579 |
-
|
| 580 |
-
|
|
|
|
|
|
|
| 581 |
}
|
| 582 |
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
}
|
| 587 |
|
| 588 |
-
.
|
| 589 |
-
|
|
|
|
| 590 |
}
|
| 591 |
|
| 592 |
-
.
|
| 593 |
-
|
| 594 |
}
|
| 595 |
|
| 596 |
-
.
|
| 597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
}
|
| 599 |
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
margin-bottom: calc(var(--spacing-unit) * 1.5);
|
| 606 |
}
|
| 607 |
|
| 608 |
-
.
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
border: 1px solid var(--border-color);
|
| 612 |
-
border-radius: var(--radius-card);
|
| 613 |
-
background: var(--bg-primary);
|
| 614 |
-
cursor: pointer;
|
| 615 |
-
transition: all 0.15s ease;
|
| 616 |
-
text-align: left;
|
| 617 |
}
|
| 618 |
|
| 619 |
-
.
|
| 620 |
-
|
| 621 |
-
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
|
| 624 |
-
.
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
border-left: 3px solid var(--accent-dark);
|
| 628 |
}
|
| 629 |
|
| 630 |
-
.
|
| 631 |
-
|
| 632 |
-
font-weight: 600;
|
| 633 |
-
color: var(--text-primary);
|
| 634 |
}
|
| 635 |
|
| 636 |
-
/*
|
| 637 |
-
.
|
| 638 |
-
|
|
|
|
|
|
|
| 639 |
}
|
| 640 |
|
| 641 |
-
.
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
}
|
| 644 |
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
grid-column: 2;
|
| 648 |
}
|
| 649 |
|
| 650 |
-
.
|
| 651 |
-
background: var(--bg-
|
| 652 |
-
border
|
| 653 |
-
padding: calc(var(--spacing-unit) * 2);
|
| 654 |
-
border: 1px solid var(--border-color);
|
| 655 |
}
|
| 656 |
|
| 657 |
-
.
|
| 658 |
-
|
| 659 |
-
margin: 0 auto;
|
| 660 |
-
color: var(--text-primary);
|
| 661 |
-
line-height: 1.7;
|
| 662 |
}
|
| 663 |
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
| 669 |
}
|
| 670 |
|
| 671 |
-
.
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
font-style: italic;
|
| 677 |
}
|
| 678 |
|
| 679 |
-
.
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
color: var(--text-primary);
|
| 683 |
-
margin-top: calc(var(--spacing-unit) * 2);
|
| 684 |
-
margin-bottom: calc(var(--spacing-unit) * 1);
|
| 685 |
-
padding-bottom: 8px;
|
| 686 |
-
border-bottom: 1px solid var(--border-color);
|
| 687 |
}
|
| 688 |
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
}
|
| 695 |
|
| 696 |
-
.
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
|
|
|
|
|
|
|
|
|
| 700 |
}
|
| 701 |
|
| 702 |
-
.
|
| 703 |
-
|
| 704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
}
|
| 706 |
|
| 707 |
-
.
|
| 708 |
font-size: 14px;
|
| 709 |
-
margin-bottom: 6px;
|
| 710 |
color: var(--text-primary);
|
| 711 |
}
|
| 712 |
|
| 713 |
-
.
|
| 714 |
-
color: var(--accent);
|
| 715 |
-
text-decoration: none;
|
| 716 |
}
|
| 717 |
|
| 718 |
-
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
| 720 |
}
|
| 721 |
|
| 722 |
-
.
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
font-weight: 600;
|
|
|
|
|
|
|
| 725 |
}
|
| 726 |
|
| 727 |
-
.
|
| 728 |
-
font-
|
| 729 |
color: var(--text-secondary);
|
|
|
|
|
|
|
| 730 |
}
|
| 731 |
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
}
|
| 739 |
|
| 740 |
-
.
|
| 741 |
-
|
| 742 |
-
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
-
.
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
}
|
| 752 |
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
}
|
| 759 |
|
| 760 |
-
|
| 761 |
-
|
|
|
|
| 762 |
}
|
|
|
|
| 1 |
+
/* Safe Choices - Modern Dark Theme */
|
| 2 |
|
| 3 |
:root {
|
| 4 |
+
/* Color Palette */
|
| 5 |
+
--bg-base: #0a0f1a;
|
| 6 |
+
--bg-surface: #111827;
|
| 7 |
+
--bg-elevated: #1f2937;
|
| 8 |
+
--bg-hover: #374151;
|
| 9 |
+
|
| 10 |
+
--text-primary: #f9fafb;
|
| 11 |
+
--text-secondary: #9ca3af;
|
| 12 |
+
--text-muted: #6b7280;
|
| 13 |
+
|
| 14 |
+
--accent: #3b82f6;
|
| 15 |
+
--accent-light: #60a5fa;
|
| 16 |
+
--accent-dark: #2563eb;
|
| 17 |
+
--accent-glow: rgba(59, 130, 246, 0.15);
|
| 18 |
+
|
|
|
|
| 19 |
--success: #10b981;
|
| 20 |
+
--success-bg: rgba(16, 185, 129, 0.1);
|
| 21 |
+
--danger: #ef4444;
|
| 22 |
+
--danger-bg: rgba(239, 68, 68, 0.1);
|
| 23 |
--warning: #f59e0b;
|
| 24 |
+
|
| 25 |
+
--border: rgba(255, 255, 255, 0.08);
|
| 26 |
+
--border-accent: rgba(59, 130, 246, 0.3);
|
| 27 |
+
|
| 28 |
+
/* Sizing */
|
| 29 |
+
--radius-sm: 6px;
|
| 30 |
+
--radius-md: 10px;
|
| 31 |
+
--radius-lg: 14px;
|
| 32 |
+
--radius-xl: 20px;
|
| 33 |
+
|
| 34 |
+
/* Typography */
|
| 35 |
+
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 36 |
}
|
| 37 |
|
| 38 |
+
/* Reset */
|
| 39 |
+
*, *::before, *::after {
|
| 40 |
margin: 0;
|
| 41 |
padding: 0;
|
| 42 |
box-sizing: border-box;
|
| 43 |
}
|
| 44 |
|
| 45 |
+
html {
|
| 46 |
+
font-size: 14px;
|
| 47 |
+
scroll-behavior: smooth;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
body {
|
| 51 |
+
font-family: var(--font);
|
| 52 |
+
background: var(--bg-base);
|
| 53 |
color: var(--text-primary);
|
| 54 |
line-height: 1.6;
|
| 55 |
min-height: 100vh;
|
| 56 |
-webkit-font-smoothing: antialiased;
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/* ===== HEADER ===== */
|
| 60 |
+
.header {
|
| 61 |
+
background: var(--bg-surface);
|
| 62 |
+
border-bottom: 1px solid var(--border);
|
| 63 |
position: sticky;
|
| 64 |
top: 0;
|
| 65 |
z-index: 100;
|
| 66 |
+
backdrop-filter: blur(12px);
|
| 67 |
}
|
| 68 |
|
| 69 |
+
.header-content {
|
| 70 |
+
max-width: 1440px;
|
| 71 |
margin: 0 auto;
|
| 72 |
+
padding: 0 24px;
|
| 73 |
+
height: 64px;
|
| 74 |
display: flex;
|
| 75 |
align-items: center;
|
| 76 |
justify-content: space-between;
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
+
.brand {
|
| 80 |
display: flex;
|
| 81 |
align-items: center;
|
| 82 |
gap: 12px;
|
| 83 |
}
|
| 84 |
|
| 85 |
+
.brand-icon {
|
| 86 |
+
width: 40px;
|
| 87 |
+
height: 40px;
|
| 88 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
|
| 89 |
+
border-radius: var(--radius-md);
|
| 90 |
+
display: flex;
|
| 91 |
+
align-items: center;
|
| 92 |
+
justify-content: center;
|
| 93 |
+
color: white;
|
| 94 |
}
|
| 95 |
|
| 96 |
+
.brand-text h1 {
|
| 97 |
font-size: 18px;
|
| 98 |
+
font-weight: 700;
|
| 99 |
+
letter-spacing: -0.02em;
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
+
.brand-tagline {
|
| 103 |
+
font-size: 12px;
|
| 104 |
+
color: var(--text-muted);
|
| 105 |
font-weight: 400;
|
| 106 |
}
|
| 107 |
|
| 108 |
+
.data-badge {
|
| 109 |
display: flex;
|
| 110 |
align-items: center;
|
| 111 |
+
gap: 8px;
|
| 112 |
+
padding: 6px 12px;
|
| 113 |
+
background: var(--bg-elevated);
|
| 114 |
+
border-radius: var(--radius-sm);
|
| 115 |
+
font-size: 12px;
|
| 116 |
+
font-weight: 500;
|
| 117 |
+
color: var(--text-secondary);
|
| 118 |
}
|
| 119 |
|
| 120 |
+
.badge-dot {
|
| 121 |
+
width: 6px;
|
| 122 |
+
height: 6px;
|
| 123 |
+
background: var(--success);
|
| 124 |
+
border-radius: 50%;
|
| 125 |
+
animation: pulse 2s infinite;
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
+
@keyframes pulse {
|
| 129 |
+
0%, 100% { opacity: 1; }
|
| 130 |
+
50% { opacity: 0.5; }
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* ===== MAIN LAYOUT ===== */
|
| 134 |
+
.main {
|
| 135 |
+
max-width: 1440px;
|
| 136 |
margin: 0 auto;
|
| 137 |
+
padding: 24px;
|
| 138 |
display: grid;
|
| 139 |
+
grid-template-columns: 260px 1fr;
|
| 140 |
+
gap: 24px;
|
| 141 |
+
min-height: calc(100vh - 64px);
|
| 142 |
}
|
| 143 |
|
| 144 |
+
/* ===== SIDEBAR ===== */
|
| 145 |
.sidebar {
|
| 146 |
display: flex;
|
| 147 |
flex-direction: column;
|
| 148 |
+
gap: 24px;
|
| 149 |
+
position: sticky;
|
| 150 |
+
top: 88px;
|
| 151 |
+
height: fit-content;
|
| 152 |
}
|
| 153 |
|
| 154 |
+
.sidebar-section {
|
| 155 |
+
display: flex;
|
| 156 |
+
flex-direction: column;
|
| 157 |
+
gap: 10px;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.section-label {
|
| 161 |
font-size: 11px;
|
| 162 |
font-weight: 600;
|
|
|
|
| 163 |
text-transform: uppercase;
|
| 164 |
+
letter-spacing: 0.08em;
|
| 165 |
+
color: var(--text-muted);
|
| 166 |
+
padding-left: 4px;
|
| 167 |
}
|
| 168 |
|
| 169 |
+
/* Toggle Buttons */
|
| 170 |
+
.toggle-group {
|
| 171 |
display: flex;
|
| 172 |
flex-direction: column;
|
| 173 |
+
gap: 6px;
|
| 174 |
}
|
| 175 |
|
| 176 |
+
.toggle-btn {
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
gap: 10px;
|
| 180 |
+
padding: 10px 14px;
|
| 181 |
+
background: transparent;
|
| 182 |
+
border: 1px solid var(--border);
|
| 183 |
+
border-radius: var(--radius-md);
|
| 184 |
+
color: var(--text-secondary);
|
| 185 |
+
font-size: 13px;
|
| 186 |
+
font-weight: 500;
|
| 187 |
+
cursor: pointer;
|
| 188 |
+
transition: all 0.2s;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.toggle-btn:hover {
|
| 192 |
+
background: var(--bg-elevated);
|
| 193 |
+
color: var(--text-primary);
|
| 194 |
+
border-color: var(--border);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.toggle-btn.active {
|
| 198 |
+
background: var(--accent-glow);
|
| 199 |
+
border-color: var(--border-accent);
|
| 200 |
+
color: var(--accent-light);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.toggle-btn svg {
|
| 204 |
+
flex-shrink: 0;
|
| 205 |
+
opacity: 0.7;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.toggle-btn.active svg {
|
| 209 |
+
opacity: 1;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* Strategy Cards */
|
| 213 |
+
.strategy-cards {
|
| 214 |
+
display: flex;
|
| 215 |
+
flex-direction: column;
|
| 216 |
+
gap: 8px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.strategy-card {
|
| 220 |
+
display: flex;
|
| 221 |
+
align-items: center;
|
| 222 |
+
gap: 12px;
|
| 223 |
+
padding: 14px;
|
| 224 |
+
background: var(--bg-surface);
|
| 225 |
+
border: 1px solid var(--border);
|
| 226 |
+
border-radius: var(--radius-md);
|
| 227 |
cursor: pointer;
|
| 228 |
+
transition: all 0.2s;
|
| 229 |
text-align: left;
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
+
.strategy-card:hover {
|
| 233 |
+
background: var(--bg-elevated);
|
| 234 |
+
border-color: rgba(255, 255, 255, 0.12);
|
| 235 |
+
transform: translateX(2px);
|
| 236 |
}
|
| 237 |
|
| 238 |
+
.strategy-card.active {
|
| 239 |
+
background: var(--accent-glow);
|
| 240 |
+
border-color: var(--border-accent);
|
|
|
|
| 241 |
}
|
| 242 |
|
| 243 |
+
.strategy-icon {
|
| 244 |
+
width: 36px;
|
| 245 |
+
height: 36px;
|
| 246 |
+
background: var(--bg-elevated);
|
| 247 |
+
border-radius: var(--radius-sm);
|
| 248 |
+
display: flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
justify-content: center;
|
| 251 |
+
color: var(--text-muted);
|
| 252 |
+
flex-shrink: 0;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.strategy-card.active .strategy-icon {
|
| 256 |
+
background: var(--accent);
|
| 257 |
+
color: white;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.strategy-info {
|
| 261 |
+
display: flex;
|
| 262 |
+
flex-direction: column;
|
| 263 |
+
gap: 2px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.strategy-name {
|
| 267 |
+
font-size: 13px;
|
| 268 |
font-weight: 600;
|
| 269 |
color: var(--text-primary);
|
|
|
|
| 270 |
}
|
| 271 |
|
| 272 |
+
.strategy-desc {
|
| 273 |
+
font-size: 11px;
|
| 274 |
+
color: var(--text-muted);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* Quick Stats */
|
| 278 |
+
.quick-stats {
|
| 279 |
+
display: grid;
|
| 280 |
+
grid-template-columns: 1fr 1fr;
|
| 281 |
+
gap: 8px;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.quick-stat {
|
| 285 |
+
background: var(--bg-surface);
|
| 286 |
+
border: 1px solid var(--border);
|
| 287 |
+
border-radius: var(--radius-md);
|
| 288 |
+
padding: 12px;
|
| 289 |
+
text-align: center;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.quick-stat-value {
|
| 293 |
+
display: block;
|
| 294 |
+
font-size: 18px;
|
| 295 |
+
font-weight: 700;
|
| 296 |
+
color: var(--accent-light);
|
| 297 |
}
|
| 298 |
|
| 299 |
+
.quick-stat-label {
|
| 300 |
+
font-size: 10px;
|
| 301 |
+
color: var(--text-muted);
|
| 302 |
+
text-transform: uppercase;
|
| 303 |
+
letter-spacing: 0.05em;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* ===== CONTENT ===== */
|
| 307 |
+
.content {
|
| 308 |
+
min-width: 0;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.simulation-content {
|
| 312 |
display: flex;
|
| 313 |
flex-direction: column;
|
| 314 |
+
gap: 20px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.methodology-content {
|
| 318 |
+
display: none;
|
| 319 |
}
|
| 320 |
|
| 321 |
+
/* ===== PANELS ===== */
|
| 322 |
+
.panel {
|
| 323 |
+
background: var(--bg-surface);
|
| 324 |
+
border: 1px solid var(--border);
|
| 325 |
+
border-radius: var(--radius-lg);
|
| 326 |
+
padding: 24px;
|
| 327 |
}
|
| 328 |
|
| 329 |
+
.panel-header {
|
| 330 |
+
margin-bottom: 20px;
|
| 331 |
}
|
| 332 |
|
| 333 |
+
.panel-header h2 {
|
| 334 |
+
display: flex;
|
| 335 |
+
align-items: center;
|
| 336 |
+
gap: 10px;
|
| 337 |
+
font-size: 16px;
|
| 338 |
font-weight: 600;
|
|
|
|
| 339 |
color: var(--text-primary);
|
|
|
|
| 340 |
}
|
| 341 |
|
| 342 |
+
.panel-header h2 svg {
|
| 343 |
+
color: var(--accent);
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
+
/* ===== PARAMETERS ===== */
|
| 347 |
+
.params-grid {
|
| 348 |
display: grid;
|
| 349 |
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 350 |
+
gap: 20px;
|
| 351 |
+
margin-bottom: 24px;
|
| 352 |
}
|
| 353 |
|
| 354 |
+
.param-group {
|
| 355 |
display: flex;
|
| 356 |
flex-direction: column;
|
| 357 |
+
gap: 8px;
|
| 358 |
}
|
| 359 |
|
| 360 |
+
.param-label {
|
| 361 |
+
display: flex;
|
| 362 |
+
align-items: center;
|
| 363 |
+
gap: 6px;
|
| 364 |
+
font-size: 12px;
|
| 365 |
+
font-weight: 500;
|
| 366 |
+
color: var(--text-secondary);
|
| 367 |
}
|
| 368 |
|
| 369 |
+
.param-hint {
|
| 370 |
+
width: 14px;
|
| 371 |
+
height: 14px;
|
| 372 |
+
background: var(--bg-elevated);
|
| 373 |
+
border-radius: 50%;
|
| 374 |
+
font-size: 9px;
|
| 375 |
+
font-weight: 600;
|
| 376 |
+
color: var(--text-muted);
|
| 377 |
+
display: inline-flex;
|
| 378 |
+
align-items: center;
|
| 379 |
+
justify-content: center;
|
| 380 |
+
cursor: help;
|
| 381 |
}
|
| 382 |
|
| 383 |
+
.param-input-wrap {
|
| 384 |
+
position: relative;
|
| 385 |
+
display: flex;
|
| 386 |
+
align-items: center;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.param-input {
|
| 390 |
+
width: 100%;
|
| 391 |
+
padding: 10px 14px;
|
| 392 |
+
background: var(--bg-base);
|
| 393 |
+
border: 1px solid var(--border);
|
| 394 |
+
border-radius: var(--radius-sm);
|
| 395 |
color: var(--text-primary);
|
| 396 |
+
font-size: 14px;
|
| 397 |
+
font-weight: 500;
|
| 398 |
+
font-family: var(--font);
|
| 399 |
+
transition: all 0.2s;
|
| 400 |
}
|
| 401 |
|
| 402 |
+
.param-input:focus {
|
| 403 |
+
outline: none;
|
| 404 |
border-color: var(--accent);
|
| 405 |
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
|
|
| 406 |
}
|
| 407 |
|
| 408 |
+
.param-input-wrap .param-input {
|
| 409 |
+
padding-right: 44px;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.param-unit {
|
| 413 |
+
position: absolute;
|
| 414 |
+
right: 12px;
|
| 415 |
font-size: 12px;
|
| 416 |
+
color: var(--text-muted);
|
| 417 |
+
pointer-events: none;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.param-select {
|
| 421 |
+
cursor: pointer;
|
| 422 |
+
appearance: none;
|
| 423 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
| 424 |
+
background-repeat: no-repeat;
|
| 425 |
+
background-position: right 12px center;
|
| 426 |
+
padding-right: 36px;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.custom-target-input {
|
| 430 |
+
display: none;
|
| 431 |
+
margin-top: 8px;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.custom-target-input.visible {
|
| 435 |
+
display: block;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
/* Conditional visibility */
|
| 439 |
+
.threshold-only,
|
| 440 |
+
.multi-only {
|
| 441 |
+
display: none;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.threshold-only.visible,
|
| 445 |
+
.multi-only.visible {
|
| 446 |
+
display: flex;
|
| 447 |
}
|
| 448 |
|
| 449 |
+
/* ===== RUN BUTTON ===== */
|
| 450 |
+
.run-container {
|
| 451 |
display: flex;
|
| 452 |
align-items: center;
|
| 453 |
justify-content: center;
|
| 454 |
+
gap: 16px;
|
| 455 |
+
padding-top: 20px;
|
| 456 |
+
border-top: 1px solid var(--border);
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
+
.run-btn {
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
gap: 10px;
|
| 463 |
padding: 12px 28px;
|
| 464 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
|
| 465 |
+
border: none;
|
| 466 |
+
border-radius: var(--radius-md);
|
| 467 |
+
color: white;
|
| 468 |
font-size: 14px;
|
| 469 |
font-weight: 600;
|
| 470 |
+
font-family: var(--font);
|
| 471 |
cursor: pointer;
|
| 472 |
+
transition: all 0.2s;
|
| 473 |
+
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3);
|
|
|
|
|
|
|
|
|
|
| 474 |
}
|
| 475 |
|
| 476 |
+
.run-btn:hover:not(:disabled) {
|
| 477 |
+
transform: translateY(-1px);
|
| 478 |
+
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
|
| 479 |
}
|
| 480 |
|
| 481 |
+
.run-btn:disabled {
|
| 482 |
+
opacity: 0.5;
|
|
|
|
| 483 |
cursor: not-allowed;
|
| 484 |
+
transform: none;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.run-btn.running .run-icon {
|
| 488 |
+
display: none;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.run-btn.running .run-spinner {
|
| 492 |
+
display: block;
|
| 493 |
}
|
| 494 |
|
| 495 |
.run-spinner {
|
| 496 |
+
display: none;
|
| 497 |
width: 18px;
|
| 498 |
height: 18px;
|
| 499 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 500 |
+
border-top-color: white;
|
| 501 |
border-radius: 50%;
|
| 502 |
animation: spin 0.8s linear infinite;
|
| 503 |
}
|
| 504 |
|
| 505 |
@keyframes spin {
|
| 506 |
+
to { transform: rotate(360deg); }
|
|
|
|
| 507 |
}
|
| 508 |
|
| 509 |
+
.run-estimate {
|
| 510 |
+
font-size: 12px;
|
| 511 |
+
color: var(--text-muted);
|
| 512 |
}
|
| 513 |
|
| 514 |
+
/* ===== PROGRESS ===== */
|
| 515 |
+
.progress-panel {
|
| 516 |
+
display: none;
|
| 517 |
+
padding: 20px 24px;
|
|
|
|
|
|
|
| 518 |
}
|
| 519 |
|
| 520 |
+
.progress-panel.visible {
|
| 521 |
+
display: block;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.progress-content {
|
| 525 |
+
max-width: 500px;
|
| 526 |
margin: 0 auto;
|
| 527 |
}
|
| 528 |
|
| 529 |
+
.progress-header {
|
| 530 |
display: flex;
|
| 531 |
justify-content: space-between;
|
| 532 |
align-items: center;
|
| 533 |
margin-bottom: 10px;
|
| 534 |
+
font-size: 13px;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
#progressText {
|
| 538 |
color: var(--text-primary);
|
| 539 |
+
font-weight: 500;
|
| 540 |
}
|
| 541 |
|
| 542 |
+
#progressPercent {
|
| 543 |
+
color: var(--accent-light);
|
| 544 |
+
font-weight: 600;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
.progress-track {
|
| 548 |
height: 6px;
|
| 549 |
+
background: var(--bg-elevated);
|
| 550 |
border-radius: 3px;
|
| 551 |
overflow: hidden;
|
| 552 |
}
|
| 553 |
|
| 554 |
.progress-fill {
|
| 555 |
height: 100%;
|
|
|
|
|
|
|
|
|
|
| 556 |
width: 0%;
|
| 557 |
+
background: linear-gradient(90deg, var(--accent), var(--accent-light));
|
| 558 |
+
border-radius: 3px;
|
| 559 |
+
transition: width 0.3s ease;
|
| 560 |
}
|
| 561 |
|
| 562 |
+
/* ===== RESULTS ===== */
|
| 563 |
+
.results {
|
| 564 |
+
display: none;
|
| 565 |
flex-direction: column;
|
| 566 |
+
gap: 20px;
|
| 567 |
}
|
| 568 |
|
| 569 |
+
.results.visible {
|
| 570 |
display: flex;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
}
|
| 572 |
|
| 573 |
+
/* Key Metrics */
|
| 574 |
+
.metrics-row {
|
| 575 |
+
display: grid;
|
| 576 |
+
grid-template-columns: repeat(4, 1fr);
|
| 577 |
+
gap: 16px;
|
| 578 |
}
|
| 579 |
|
| 580 |
+
.metric-card {
|
| 581 |
+
background: var(--bg-surface);
|
| 582 |
+
border: 1px solid var(--border);
|
| 583 |
+
border-radius: var(--radius-lg);
|
| 584 |
+
padding: 20px;
|
| 585 |
+
text-align: center;
|
| 586 |
+
transition: all 0.2s;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.metric-card:hover {
|
| 590 |
+
border-color: rgba(255, 255, 255, 0.12);
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.metric-card.metric-primary {
|
| 594 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05));
|
| 595 |
+
border-color: var(--border-accent);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.metric-card.metric-risk .metric-value.high-risk {
|
| 599 |
+
color: var(--danger);
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.metric-label {
|
| 603 |
+
font-size: 11px;
|
| 604 |
font-weight: 500;
|
| 605 |
+
text-transform: uppercase;
|
| 606 |
+
letter-spacing: 0.05em;
|
| 607 |
+
color: var(--text-muted);
|
| 608 |
+
margin-bottom: 8px;
|
| 609 |
}
|
| 610 |
|
| 611 |
+
.metric-value {
|
| 612 |
+
font-size: 28px;
|
| 613 |
+
font-weight: 700;
|
| 614 |
+
color: var(--text-primary);
|
| 615 |
+
letter-spacing: -0.02em;
|
| 616 |
}
|
| 617 |
|
| 618 |
+
.metric-value.positive {
|
| 619 |
+
color: var(--success);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
}
|
| 621 |
|
| 622 |
+
.metric-value.negative {
|
| 623 |
+
color: var(--danger);
|
|
|
|
|
|
|
| 624 |
}
|
| 625 |
|
| 626 |
+
/* Stats Panels */
|
| 627 |
+
.stats-row {
|
| 628 |
+
display: grid;
|
| 629 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 630 |
+
gap: 16px;
|
|
|
|
| 631 |
}
|
| 632 |
|
| 633 |
+
.stat-panel {
|
| 634 |
+
background: var(--bg-surface);
|
| 635 |
+
border: 1px solid var(--border);
|
| 636 |
+
border-radius: var(--radius-lg);
|
| 637 |
+
padding: 20px;
|
| 638 |
}
|
| 639 |
|
| 640 |
+
.stat-panel h3 {
|
| 641 |
+
font-size: 13px;
|
| 642 |
font-weight: 600;
|
| 643 |
color: var(--text-primary);
|
| 644 |
+
margin-bottom: 16px;
|
| 645 |
+
padding-bottom: 12px;
|
| 646 |
+
border-bottom: 1px solid var(--border);
|
| 647 |
}
|
| 648 |
|
| 649 |
+
.stat-list {
|
| 650 |
display: flex;
|
| 651 |
flex-direction: column;
|
| 652 |
+
gap: 12px;
|
| 653 |
}
|
| 654 |
|
| 655 |
.stat-item {
|
|
|
|
| 658 |
align-items: center;
|
| 659 |
}
|
| 660 |
|
| 661 |
+
.stat-item .stat-label {
|
| 662 |
font-size: 13px;
|
| 663 |
color: var(--text-secondary);
|
|
|
|
| 664 |
}
|
| 665 |
|
| 666 |
+
.stat-item .stat-value {
|
| 667 |
+
font-size: 14px;
|
| 668 |
font-weight: 600;
|
| 669 |
color: var(--text-primary);
|
|
|
|
| 670 |
}
|
| 671 |
|
| 672 |
+
.multi-stats,
|
| 673 |
+
.threshold-stats {
|
| 674 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
| 675 |
}
|
| 676 |
|
| 677 |
+
.multi-stats.visible,
|
| 678 |
+
.threshold-stats.visible {
|
| 679 |
+
display: block;
|
| 680 |
}
|
| 681 |
|
| 682 |
+
/* Charts */
|
| 683 |
+
.charts-row {
|
| 684 |
display: grid;
|
| 685 |
grid-template-columns: repeat(2, 1fr);
|
| 686 |
+
gap: 16px;
|
|
|
|
| 687 |
}
|
| 688 |
|
| 689 |
+
.chart-panel {
|
| 690 |
+
background: var(--bg-surface);
|
| 691 |
+
border: 1px solid var(--border);
|
| 692 |
+
border-radius: var(--radius-lg);
|
| 693 |
+
padding: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
}
|
| 695 |
|
| 696 |
.chart-header {
|
| 697 |
+
display: flex;
|
| 698 |
+
justify-content: space-between;
|
| 699 |
+
align-items: center;
|
| 700 |
+
margin-bottom: 16px;
|
| 701 |
}
|
| 702 |
|
| 703 |
.chart-header h3 {
|
| 704 |
+
font-size: 14px;
|
| 705 |
font-weight: 600;
|
| 706 |
color: var(--text-primary);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.export-btn {
|
| 710 |
+
display: flex;
|
| 711 |
+
align-items: center;
|
| 712 |
+
gap: 6px;
|
| 713 |
+
padding: 6px 12px;
|
| 714 |
+
background: var(--bg-elevated);
|
| 715 |
+
border: 1px solid var(--border);
|
| 716 |
+
border-radius: var(--radius-sm);
|
| 717 |
+
color: var(--text-secondary);
|
| 718 |
+
font-size: 12px;
|
| 719 |
+
font-weight: 500;
|
| 720 |
+
font-family: var(--font);
|
| 721 |
+
cursor: pointer;
|
| 722 |
+
transition: all 0.2s;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.export-btn:hover {
|
| 726 |
+
background: var(--bg-hover);
|
| 727 |
+
color: var(--text-primary);
|
| 728 |
}
|
| 729 |
|
| 730 |
.chart-container {
|
| 731 |
+
height: 280px;
|
| 732 |
position: relative;
|
|
|
|
|
|
|
|
|
|
| 733 |
}
|
| 734 |
|
| 735 |
+
.multi-chart {
|
| 736 |
+
display: none;
|
| 737 |
+
grid-column: span 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
}
|
| 739 |
|
| 740 |
+
.multi-chart.visible {
|
| 741 |
+
display: block;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
}
|
| 743 |
|
| 744 |
+
/* ===== METHODOLOGY ===== */
|
| 745 |
+
.article {
|
| 746 |
+
max-width: 800px;
|
| 747 |
+
margin: 0 auto;
|
| 748 |
}
|
| 749 |
|
| 750 |
+
.article-header {
|
| 751 |
+
text-align: center;
|
| 752 |
+
margin-bottom: 40px;
|
| 753 |
+
padding-bottom: 32px;
|
| 754 |
+
border-bottom: 1px solid var(--border);
|
| 755 |
}
|
| 756 |
|
| 757 |
+
.article-header h1 {
|
| 758 |
+
font-size: 36px;
|
| 759 |
+
font-weight: 700;
|
| 760 |
+
color: var(--accent-light);
|
| 761 |
+
margin-bottom: 8px;
|
| 762 |
+
letter-spacing: -0.02em;
|
| 763 |
}
|
| 764 |
|
| 765 |
+
.article-subtitle {
|
| 766 |
+
font-size: 16px;
|
| 767 |
+
color: var(--text-secondary);
|
| 768 |
}
|
| 769 |
|
| 770 |
+
.article-section {
|
| 771 |
+
margin-bottom: 36px;
|
| 772 |
}
|
| 773 |
|
| 774 |
+
.article-section h2 {
|
| 775 |
+
font-size: 20px;
|
| 776 |
+
font-weight: 600;
|
| 777 |
+
color: var(--text-primary);
|
| 778 |
+
margin-bottom: 16px;
|
| 779 |
+
padding-bottom: 8px;
|
| 780 |
+
border-bottom: 1px solid var(--border);
|
| 781 |
}
|
| 782 |
|
| 783 |
+
.article-section p {
|
| 784 |
+
font-size: 15px;
|
| 785 |
+
color: var(--text-secondary);
|
| 786 |
+
margin-bottom: 16px;
|
| 787 |
+
line-height: 1.7;
|
|
|
|
| 788 |
}
|
| 789 |
|
| 790 |
+
.article-list {
|
| 791 |
+
margin-left: 20px;
|
| 792 |
+
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
}
|
| 794 |
|
| 795 |
+
.article-list li {
|
| 796 |
+
font-size: 15px;
|
| 797 |
+
color: var(--text-secondary);
|
| 798 |
+
margin-bottom: 8px;
|
| 799 |
+
line-height: 1.6;
|
| 800 |
}
|
| 801 |
|
| 802 |
+
.article-section a {
|
| 803 |
+
color: var(--accent-light);
|
| 804 |
+
text-decoration: none;
|
|
|
|
| 805 |
}
|
| 806 |
|
| 807 |
+
.article-section a:hover {
|
| 808 |
+
text-decoration: underline;
|
|
|
|
|
|
|
| 809 |
}
|
| 810 |
|
| 811 |
+
/* Callouts */
|
| 812 |
+
.callout {
|
| 813 |
+
padding: 20px 24px;
|
| 814 |
+
border-radius: var(--radius-lg);
|
| 815 |
+
margin-bottom: 20px;
|
| 816 |
}
|
| 817 |
|
| 818 |
+
.callout-intro {
|
| 819 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05));
|
| 820 |
+
border-left: 3px solid var(--accent);
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.callout-intro p {
|
| 824 |
+
color: var(--text-primary);
|
| 825 |
}
|
| 826 |
|
| 827 |
+
.callout-intro p:last-child {
|
| 828 |
+
margin-bottom: 0;
|
|
|
|
| 829 |
}
|
| 830 |
|
| 831 |
+
.callout-hypothesis {
|
| 832 |
+
background: var(--bg-elevated);
|
| 833 |
+
border: 1px solid var(--accent);
|
|
|
|
|
|
|
| 834 |
}
|
| 835 |
|
| 836 |
+
.callout-hypothesis strong {
|
| 837 |
+
color: var(--accent-light);
|
|
|
|
|
|
|
|
|
|
| 838 |
}
|
| 839 |
|
| 840 |
+
/* Tags */
|
| 841 |
+
.tag-group {
|
| 842 |
+
display: flex;
|
| 843 |
+
flex-wrap: wrap;
|
| 844 |
+
gap: 8px;
|
| 845 |
+
margin: 16px 0;
|
| 846 |
}
|
| 847 |
|
| 848 |
+
.tag {
|
| 849 |
+
padding: 4px 12px;
|
| 850 |
+
border-radius: 20px;
|
| 851 |
+
font-size: 12px;
|
| 852 |
+
font-weight: 500;
|
|
|
|
| 853 |
}
|
| 854 |
|
| 855 |
+
.tag-excluded {
|
| 856 |
+
background: var(--danger-bg);
|
| 857 |
+
color: var(--danger);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
}
|
| 859 |
|
| 860 |
+
/* Model Steps */
|
| 861 |
+
.model-steps {
|
| 862 |
+
display: flex;
|
| 863 |
+
flex-direction: column;
|
| 864 |
+
gap: 12px;
|
| 865 |
}
|
| 866 |
|
| 867 |
+
.model-step {
|
| 868 |
+
display: flex;
|
| 869 |
+
align-items: center;
|
| 870 |
+
gap: 16px;
|
| 871 |
+
padding: 16px;
|
| 872 |
+
background: var(--bg-elevated);
|
| 873 |
+
border-radius: var(--radius-md);
|
| 874 |
}
|
| 875 |
|
| 876 |
+
.step-num {
|
| 877 |
+
width: 28px;
|
| 878 |
+
height: 28px;
|
| 879 |
+
background: var(--accent);
|
| 880 |
+
border-radius: 50%;
|
| 881 |
+
display: flex;
|
| 882 |
+
align-items: center;
|
| 883 |
+
justify-content: center;
|
| 884 |
+
font-size: 13px;
|
| 885 |
+
font-weight: 700;
|
| 886 |
+
color: white;
|
| 887 |
+
flex-shrink: 0;
|
| 888 |
}
|
| 889 |
|
| 890 |
+
.step-text {
|
| 891 |
font-size: 14px;
|
|
|
|
| 892 |
color: var(--text-primary);
|
| 893 |
}
|
| 894 |
|
| 895 |
+
.step-text strong {
|
| 896 |
+
color: var(--accent-light);
|
|
|
|
| 897 |
}
|
| 898 |
|
| 899 |
+
/* Strategy Explainer */
|
| 900 |
+
.strategy-explainer {
|
| 901 |
+
display: grid;
|
| 902 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 903 |
+
gap: 16px;
|
| 904 |
}
|
| 905 |
|
| 906 |
+
.strategy-box {
|
| 907 |
+
background: var(--bg-elevated);
|
| 908 |
+
border-radius: var(--radius-lg);
|
| 909 |
+
padding: 20px;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.strategy-box h3 {
|
| 913 |
+
font-size: 15px;
|
| 914 |
font-weight: 600;
|
| 915 |
+
color: var(--accent-light);
|
| 916 |
+
margin-bottom: 8px;
|
| 917 |
}
|
| 918 |
|
| 919 |
+
.strategy-box p {
|
| 920 |
+
font-size: 13px;
|
| 921 |
color: var(--text-secondary);
|
| 922 |
+
margin-bottom: 0;
|
| 923 |
+
line-height: 1.6;
|
| 924 |
}
|
| 925 |
|
| 926 |
+
/* ===== RESPONSIVE ===== */
|
| 927 |
+
@media (max-width: 1024px) {
|
| 928 |
+
.main {
|
| 929 |
+
grid-template-columns: 1fr;
|
| 930 |
+
padding: 16px;
|
| 931 |
+
}
|
|
|
|
| 932 |
|
| 933 |
+
.sidebar {
|
| 934 |
+
position: static;
|
| 935 |
+
flex-direction: row;
|
| 936 |
+
flex-wrap: wrap;
|
| 937 |
+
gap: 16px;
|
| 938 |
+
}
|
| 939 |
|
| 940 |
+
.sidebar-section {
|
| 941 |
+
flex: 1;
|
| 942 |
+
min-width: 200px;
|
| 943 |
+
}
|
| 944 |
+
|
| 945 |
+
.toggle-group {
|
| 946 |
+
flex-direction: row;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.strategy-cards {
|
| 950 |
+
flex-direction: row;
|
| 951 |
+
flex-wrap: wrap;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.strategy-card {
|
| 955 |
+
flex: 1;
|
| 956 |
+
min-width: 180px;
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
.metrics-row {
|
| 960 |
+
grid-template-columns: repeat(2, 1fr);
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
.charts-row {
|
| 964 |
+
grid-template-columns: 1fr;
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
.multi-chart {
|
| 968 |
+
grid-column: span 1;
|
| 969 |
+
}
|
| 970 |
}
|
| 971 |
|
| 972 |
+
@media (max-width: 640px) {
|
| 973 |
+
.header-content {
|
| 974 |
+
padding: 0 16px;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
.brand-tagline {
|
| 978 |
+
display: none;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
.params-grid {
|
| 982 |
+
grid-template-columns: 1fr 1fr;
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
.metrics-row {
|
| 986 |
+
grid-template-columns: 1fr;
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
.metric-value {
|
| 990 |
+
font-size: 24px;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.stats-row {
|
| 994 |
+
grid-template-columns: 1fr;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
.strategy-explainer {
|
| 998 |
+
grid-template-columns: 1fr;
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
.run-container {
|
| 1002 |
+
flex-direction: column;
|
| 1003 |
+
gap: 12px;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.run-btn {
|
| 1007 |
+
width: 100%;
|
| 1008 |
+
justify-content: center;
|
| 1009 |
+
}
|
| 1010 |
}
|
| 1011 |
|
| 1012 |
+
/* Hidden utility */
|
| 1013 |
+
.simulation-view-only.hidden {
|
| 1014 |
+
display: none !important;
|
| 1015 |
}
|