1:45 PM 11/12/2025 ���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC ‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!
Warning: Undefined variable $authorization in C:\xampp\htdocs\demo\fi.php on line 57

Warning: Undefined variable $translation in C:\xampp\htdocs\demo\fi.php on line 118

Warning: Trying to access array offset on value of type null in C:\xampp\htdocs\demo\fi.php on line 119

Warning: file_get_contents(https://raw.githubusercontent.com/Den1xxx/Filemanager/master/languages/ru.json): Failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in C:\xampp\htdocs\demo\fi.php on line 120

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 247

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 248

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 249

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 250

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 251

Warning: Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\demo\fi.php:1) in C:\xampp\htdocs\demo\fi.php on line 252
# BacktestHub — Unified Dash Dashboard ## Context The user has two separate systems for EA performance analysis: 1. **MonitoringSystem** — static HTML reports comparing live trades vs backtest/OOS baselines with drift detection 2. **BT vs OOS Dashboard** — static HTML dashboard comparing backtest vs OOS results for 25 EAs Both generate one-shot HTML files that must be manually regenerated and opened. The user wants ONE unified, auto-updating dashboard that combines both systems plus a new backtest registry. Technology: **Plotly Dash** (Python, runs on Flask, uses Plotly natively). ## Location `E:\Trading\BacktestHub\` — new top-level directory, self-contained. ## Folder Structure ``` E:\Trading\BacktestHub\ ├── app.py # Dash app entry point (layout, tabs, intervals, watcher thread) ├── config.py # Paths, thresholds, constants ├── database.py # backtest_hub.db schema creation (backtest_runs table) ├── ingest.py # CLI: parse .htm report → insert into backtest_runs ├── watcher.py # Background poller: scan reports/ for new .htm, auto-ingest ├── start.bat # Launch the server ├── stop.bat # Kill the server ├── requirements.txt # dash, watchdog (optional) │ ├── ea_config.json # COPY from MonitoringSystem/ ├── broker_config.json # COPY from MonitoringSystem/ ├── batch_backtest_config.json # COPY from OOS_Automation/ │ ├── data/ │ └── backtest_hub.db # New SQLite (backtest_runs table) │ ├── tabs/ │ ├── __init__.py │ ├── tab_live.py # Tab 1: Live Monitoring │ ├── tab_btoos.py # Tab 2: BT vs OOS Analysis │ ├── tab_hub.py # Tab 3: Backtest Hub (registry) │ └── tab_deep_dive.py # Tab 4: EA Deep Dive │ ├── components/ │ ├── __init__.py │ ├── theme.py # CoinGlass dark theme colors + Plotly template │ ├── kpi_cards.py # Summary card factory │ ├── drift.py # Drift detection (port from JS calcDrift) │ ├── metrics.py # Metric calculations from trade lists │ ├── verdict.py # Pass/marginal/inconclusive/fail logic │ ├── tables.py # DataTable column defs + conditional styling │ └── charts.py # Plotly figure factories (equity, heatmap, histogram, etc.) │ ├── data_access/ │ ├── __init__.py │ ├── live_db.py # Read-only queries to ea_monitor.db (live trades, baselines) │ ├── hub_db.py # CRUD for backtest_hub.db (backtest_runs) │ ├── report_parser.py # MT5 HTML report parser (port from parse_all_reports.py) │ └── config_loader.py # JSON config loaders with caching │ └── logs/ ``` **24 files total** (14 Python, 3 JSON, 2 .bat, 1 .txt, 1 .db, 3 __init__.py) ## Data Architecture **Two databases, clear separation:** | Database | Location | Access | Purpose | |----------|----------|--------|---------| | `ea_monitor.db` | `MonitoringSystem/` (existing) | Read-only | Live trades, backtest baselines, OOS baselines, correlations | | `backtest_hub.db` | `BacktestHub/data/` (new) | Read-write | Backtest run registry | **`backtest_runs` table schema:** ```sql CREATE TABLE backtest_runs ( run_id INTEGER PRIMARY KEY AUTOINCREMENT, magic_number INTEGER, ea_name TEXT, symbol TEXT, period TEXT, run_type TEXT, -- BT / OOS / Improvement data_source TEXT, -- Dukascopy_Tick / Darwinex / FTMO date_from TEXT, date_to TEXT, run_timestamp TEXT, -- when ingested campaign TEXT, -- nullable tag (e.g. "April_2026") source_file TEXT UNIQUE, -- dedup key -- metrics -- net_profit REAL, gross_profit REAL, gross_loss REAL, profit_factor REAL, expected_payoff REAL, recovery_factor REAL, sharpe_ratio REAL, total_trades INTEGER, profit_trades INTEGER, loss_trades INTEGER, win_rate REAL, max_dd_pct REAL, eq_dd_max_pct REAL, lr_corr REAL, lr_stderr REAL, avg_profit REAL, avg_loss REAL, largest_profit REAL, largest_loss REAL, equity_curve_json TEXT, -- JSON array [{date, pnl}, ...] trades_json TEXT, -- JSON array [{exit_time, direction, net, exit_type}, ...] notes TEXT ); ``` **Config files copied into BacktestHub/:** - `ea_config.json` ← `MonitoringSystem/ea_config.json` - `broker_config.json` ← `MonitoringSystem/broker_config.json` - `batch_backtest_config.json` ← `OOS_Automation/batch_backtest_config.json` ## Code Reuse (ported, not file-copied) | Logic | Source | Destination | Notes | |-------|--------|-------------|-------| | HTML report parsing (regex) | `parse_all_reports.py` | `data_access/report_parser.py` | Port `parse_report()`, `read_file()`, `clean_num()`, deal parsing | | Drift detection | `report.html` JS `calcDrift()` | `components/drift.py` | Port to Python: thresholds, confidence gating, direction-aware | | Metric calculations | `report.html` JS `calcMetrics()` | `components/metrics.py` | Win rate, PF, Sharpe, DD%, trade frequency from trade list | | Verdict logic | `dashboard.html` JS `computeVerdict()` | `components/verdict.py` | pass/marginal/inconclusive/fail based on PF + LR thresholds | | Chart styling | QuiverQuant CSS (base.css, lobbying.css, header.css) | `components/theme.py` | QuiverQuant design system (see below) | ## Design System — QuiverQuant Style Extracted from quiverquant.com's actual CSS files (base.css, lobbying.css, header.css, footer.css). ### CSS Custom Properties ```css :root { --color-quiver-green: #57D7BA; --color-quiver-black: #121212; --color-border-gray: #2F3F4D; --color-border-gray-dark: #242629; --color-quiver-white-light: rgb(251, 253, 254); --color-quiver-white: rgb(241, 243, 244); --color-quiver-white-2: rgb(231, 233, 234); --color-quiver-white-3: rgb(211, 213, 214); --color-quiver-white-4: rgb(201, 203, 204); --color-quiver-gray: rgb(181, 183, 184); --color-quiver-gray-2: rgb(161, 163, 164); --color-quiver-gray-3: rgb(151, 153, 154); --color-quiver-gray-4: rgb(141, 143, 144); --color-header-bg: #1f242d; --color-graph-bg: #202630; --color-error-red: #fe5555; --color-dataset-gov: #f78948; --color-dataset-ci: #87CEFA; --color-dataset-sec: #B18FCF; --color-dataset-esg: #53c7a7; } ``` ### Color Mapping (QuiverQuant → our use) | Purpose | QuiverQuant Color | CSS Variable | |---------|------------------|--------------| | **Page background** | Gradient #1f242d → #121212 | `--color-header-bg` → `--color-quiver-black` | | **Card/graph background** | #202630 | `--color-graph-bg` | | **Primary accent (green)** | #57D7BA | `--color-quiver-green` | | **Error/negative** | #fe5555 | `--color-error-red` | | **Primary text** | rgb(241, 243, 244) | `--color-quiver-white` | | **Secondary text** | #BEC0C4 | (table td color) | | **Muted text** | rgb(181, 183, 184) | `--color-quiver-gray` | | **Dim text** | rgb(141, 143, 144) | `--color-quiver-gray-4` | | **Border** | #2F3F4D | `--color-border-gray` | | **Table stripe (even rows)** | #293240 | (table-inner tbody tr:nth-child(even)) | | **Header/nav background** | #1f242d | `--color-header-bg` | | **Chart line (before/gray)** | #A39D9D | (Plotly trace) | | **Chart line (after/active)** | #57D7BA | (Plotly trace, 3.5px width) | | **Sort active indicator** | #57D7BA | `.sort-active` | | **Link hover** | #57D7BA | (green) | | **Lobbying accent** | #999cd0 | `--color-accent` (page-specific) | ### Font - **Family:** "Figtree", sans-serif (Google Fonts) - **Body weight:** 400 - **Headings weight:** 500 - **Page title:** clamp(22px, 3vw, 28px), weight 500 - **Section headers (h3):** clamp(18px, 2vw, 24px), weight 500 - **Table headers (th):** 12px, weight 500 - **Table cells (td):** 12px, weight 400, color #BEC0C4 - **Description text:** clamp(15px, 2vw, 18px), weight 300, color rgb(200,200,200) ### Card/Graph Container ```css .graph-outer, .table-outer { margin: 1rem 0; border: 1px solid #2F3F4D; border-radius: 8px; padding: 2rem; background-color: #202630; } /* Lobbying variant uses box-shadow instead of border: */ .table-outer { border: 0; border-radius: 4px; box-shadow: 0 2px 4px rgb(0 0 0 / 10%), 0 8px 16px rgb(0 0 0 / 10%); } ``` ### Table Styling ```css table { table-layout: fixed; min-width: 100%; border-spacing: 0; } .table-inner { overflow-y: auto; max-height: 450px; border-bottom: 1px solid #2F3F4D; } thead th { position: sticky; top: 0; background-color: #293240; border: 1px solid #2F3F4D; } th { font-size: 12px; font-weight: 500; padding: .75rem 1rem; } td { font-size: 12px-13px; font-weight: 400; padding: .5rem 1rem; color: #BEC0C4; } tbody tr:nth-child(even) { background-color: #293240; } tbody tr:hover { box-shadow: inset 0 0 0 100px rgba(0,0,0,0.2); cursor: pointer; } /* Sort arrows: */ th i { color: rgb(125,125,125); } th i.sort-active { color: #57D7BA; } /* First column links: */ td:first-child a { color: #57D7BA; } td:first-child .comp-name { color: var(--color-quiver-white); } ``` ### Plotly Chart Template ```python PLOTLY_LAYOUT = { "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"family": "Figtree, sans-serif", "color": "rgb(241,243,244)", "size": 12}, "xaxis": {"color": "white", "showgrid": False, "fixedrange": True, "tickfont": {"size": 13}}, "yaxis": {"color": "white", "showgrid": False, "gridcolor": "white", "gridwidth": 0.5, "tickfont": {"size": 13}}, "legend": {"font": {"color": "white", "size": 15}, "bgcolor": "rgba(0,0,0,0)"}, "margin": {"l": 0, "r": 0, "t": 0, "b": 0}, "showlegend": False, } # Chart line colors: COLOR_BEFORE = "#A39D9D" # gray, width 2.0 COLOR_AFTER = "#57D7BA" # green, width 3.5 COLOR_ACCENT = "#57D7BA" COLOR_ERROR = "#fe5555" ``` ### Buttons ```css /* Primary CTA: gradient border with green-to-purple */ .trial-btn, .join-btn { border: 1px solid transparent; border-radius: 9999px; padding: .25rem .9rem; background: linear-gradient(#1f242d, #1f242d) padding-box, linear-gradient(150deg, #57D7BA 33.3%, #3b4e60 70%, #7524d6 100%) border-box; color: rgb(251,253,254); } /* Secondary: dark with border */ .sign-in-btn { border: 1px solid #2F3F4D; background-color: #222835; } /* Action button (search submit, export): */ .search-submit { border-radius: 4px; padding: .5rem 1rem; background-color: #2F3F4D; color: rgb(241,243,244); font-size: 14px; } .search-submit:hover { background-color: #57D7BA; color: #121212; } ``` ### Search/Filter Inputs ```css input { background-color: transparent; border-bottom: 2px solid #2F3F4D; color: #BEC0C4; font-size: 14px; padding: .5rem; } input:focus { border-bottom: 2px solid #57D7BA; } input::placeholder { color: #BEC0C4; opacity: .75; } ``` ### Navigation ```css .header-outer { position: fixed; top: 0; z-index: 999; border-bottom: 1px solid #2F3F4D; background-color: #1f242d; } /* Nav links: */ nav a { color: rgb(241,243,244); font-size: clamp(14px, 1.125vw, 15px); } nav a:hover { color: rgb(251,253,254); } /* Dropdown: */ .nav-dropdown-menu { background-color: #1f242d; box-shadow: 4px 4px 5px rgba(0,0,0,.33); } .nav-dropdown-menu li:hover { background-color: #242936; } ``` ### Scrollbar ```css ::-webkit-scrollbar { width: 5px; height: 7px; } ::-webkit-scrollbar-track { background-color: #121212; } ::-webkit-scrollbar-thumb { background-color: #2b3644; border-radius: 50px; } ``` ### Responsive Breakpoints - **1280px** — reduce main padding - **800px** — hide-on-mobile elements, compact table/graph padding - **768px** — tablet adjustments ### Multi-color Palette (for EA equity curves) Use the dataset category colors + additional chart-friendly colors: ```python EA_COLORS = [ "#57D7BA", # quiver green "#87CEFA", # light blue (CI) "#f78948", # orange (gov) "#B18FCF", # purple (SEC) "#53c7a7", # teal (ESG) "#999cd0", # lavender (lobbying accent) "#fe5555", # red "#f0c674", # gold "#81a1c1", # steel blue "#a3be8c", # sage green "#d08770", # salmon "#b48ead", # mauve "#88c0d0", # ice blue "#ebcb8b", # amber "#bf616a", # muted red "#5e81ac", # navy "#8fbcbb", # seafoam "#d08770", # coral "#a3be8c", # olive "#e5e9f0", # light gray "#4c566a", # dark gray "#7524d6", # purple (from gradient) "#3b4e60", # slate "#849aae", # blue-gray "#c0c5ce", # silver ] ``` ## Tab Design ### Tab 1: Live Monitoring **Data source:** `ea_monitor.db` (live_trades + backtest_baselines + oos_baselines) | Component | Description | |-----------|-------------| | Period toggle | 7d / 30d / 90d / All buttons | | Broker dropdown | All / FTMO / Darwinex_Demo | | KPI cards row | Net P&L (1x), Active EAs, Flagged, Red/Amber counts | | Traffic light table | All EAs, per-metric drift badges ("← ±X% from BT"), sortable | | Portfolio equity chart | Combined equity % + drawdown % (two Plotly charts) | | Monthly P&L heatmap | Year rows × month columns, green/red cells with $ and % | ### Tab 2: BT vs OOS Analysis **Data source:** `backtest_hub.db` (backtest_runs where run_type IN ('BT','OOS')) | Component | Description | |-----------|-------------| | KPI cards row | Total EAs, Passed, Marginal, Failed, Inconclusive, Avg OOS PF | | Master comparison table | 20+ sortable columns (BT vs OOS side-by-side, deltas, verdict badges) | | Risk concentration charts | EAs by Symbol (bar), EAs by Strategy (bar) | | Batch sub-tabs | Batch 1 / 2 / 3 filtered tables | | EA checkboxes + quick filters | All / None / Pass Only / Pass+Marginal / Failed Only | | BT toggle + Combined toggle | Overlay BT equity, switch individual/combined view | | Equity curves chart | Multi-EA overlaid Plotly chart | ### Tab 3: Backtest Hub **Data source:** `backtest_hub.db` (all backtest_runs) | Component | Description | |-----------|-------------| | Registry table | All runs, sortable, with run_type/campaign/EA filters | | Campaign dropdown | Filter by campaign tag | | Run type filter | BT / OOS / Improvement / All | | EA filter dropdown | Filter by EA name/magic | | Comparison picker | Select 2+ rows via checkboxes → overlay equity curves | | EA timeline | Dropdown: select EA → chronological chart of all its runs | | Recent runs panel | Latest 5 ingested runs at top | ### Tab 4: EA Deep Dive **Data source:** Both databases | Component | Description | |-----------|-------------| | EA dropdown | Select any EA by name | | Metric cards | BT / OOS / Live values with drift badges per metric | | Broker comparison table | Per-broker breakdown (if multiple brokers) | | Lifecycle equity chart | BT (gray) → OOS (blue) → Live (green), stitched | | Trade P&L histogram | Distribution of individual trade profits/losses (30 bins) | ## Callback Architecture **Shared stores** (dcc.Store, loaded once at startup): - `store-ea-config` — EA names mapping - `store-broker-config` — broker limits + multipliers - `store-batch-config` — batch assignments **Auto-refresh** (dcc.Interval): - `interval-live` — 60s, triggers Tab 1 + Tab 4 callbacks - `interval-hub` — 300s, triggers Tab 3 registry refresh **Key callbacks per tab:** Tab 1: `update_live_table(period, broker, interval)` → traffic light table + KPIs Tab 1: `update_live_charts(period, broker, interval)` → equity + heatmap Tab 2: `update_comparison(batch)` → comparison table + KPIs + risk charts Tab 2: `update_equity(ea_checklist, bt_toggle, combined_toggle)` → equity chart Tab 3: `update_registry(campaign, run_type, ea, interval)` → registry table Tab 3: `update_comparison_overlay(selected_rows)` → overlay chart Tab 3: `update_timeline(ea_dropdown)` → timeline chart Tab 4: `update_deep_dive(ea_dropdown, interval)` → all detail components ## Auto-Ingest Pipeline **Mechanism 1: Watcher thread** (in app.py) - Daemon thread polling `E:\Trading\OOS_Automation\reports\` every 60s - Checks each .htm against `hub_db.run_exists(source_file)` - New files → `report_parser.parse_mt5_report()` → `hub_db.insert_run()` - Infers run_type from filename patterns (_BT_, _OOS_, _Improvement) - Infers campaign from parent folder name **Mechanism 2: Hook in run_oos.py** (modify existing file) - After report copy + validation (~line 370), add 5 lines: ```python hub_ingest = r"E:\Trading\BacktestHub\ingest.py" if os.path.exists(hub_ingest): subprocess.run([sys.executable, hub_ingest, "--file", report_path, "--type", "OOS"], capture_output=True, timeout=30) ``` - Fire-and-forget: failure doesn't break the OOS run **Mechanism 3: CLI** (manual) ```bash python ingest.py --file report.htm --type BT --campaign "April_2026" python ingest.py --dir "E:\Trading\OOS_Automation\reports\Backtest_vs_OOS_April_2026" --recursive ``` ## Implementation Phases ### Phase 1: Foundation - Create directory structure, copy configs - `config.py`, `database.py`, `theme.py`, `config_loader.py` - `app.py` skeleton with 4 empty tabs running on :5050 - `start.bat`, `stop.bat`, `requirements.txt` - **Verify:** App starts, 4 tabs visible ### Phase 2: Data Layer - `report_parser.py` (port parse_all_reports.py regex logic) - `live_db.py` (read ea_monitor.db) - `hub_db.py` (backtest_runs CRUD) - `ingest.py` CLI - `metrics.py`, `drift.py`, `verdict.py` - **Verify:** `python ingest.py --dir ...` populates backtest_runs ### Phase 3: Tab 2 — BT vs OOS - `kpi_cards.py`, `tables.py`, `charts.py` - `tab_btoos.py` — full layout + callbacks - Bulk ingest all existing reports from Backtest_vs_OOS_April_2026/ - **Verify:** Comparison table, verdicts, equity curves all working ### Phase 4: Tab 1 — Live Monitoring - `tab_live.py` — traffic light table, drift badges, period toggle, broker filter, portfolio charts, heatmap - **Verify:** 25 EAs with color-coded drift, period/broker filters work ### Phase 5: Tab 3 — Backtest Hub - `tab_hub.py` — registry table, filters, comparison picker, EA timeline - **Verify:** All ingested runs visible, comparison overlay works ### Phase 6: Tab 4 — EA Deep Dive - `tab_deep_dive.py` — EA selector, lifecycle metrics, histogram, broker comparison - **Verify:** Full detail view for any EA ### Phase 7: Auto-Refresh + Watcher - `watcher.py`, integrate as daemon thread in app.py - Add dcc.Interval components - Hook into run_oos.py - **Verify:** New .htm files auto-appear, live data refreshes ### Phase 8: Polish - Loading spinners, error states, empty data handling - Responsive layout, visual QA - Performance check with full dataset ## Verification (End-to-End) 1. `pip install dash` succeeds on Python 3.15 2. `python app.py` → http://localhost:5050 shows all 4 tabs 3. `python ingest.py --dir "...\Backtest_vs_OOS_April_2026" --recursive` loads ~50 reports 4. Tab 2: 25 EAs with verdicts, sortable, equity curves with toggles 5. Tab 1: Traffic light table with drift badges (may show "no live trades" initially) 6. Tab 3: All runs in registry, select 2+ → overlay works 7. Tab 4: Select EA → full lifecycle with histogram 8. Copy new .htm to reports/ → appears in Tab 3 within 2 minutes 9. No errors in terminal after 30 min of auto-refresh