Skip to content

Commit 495aaef

Browse files
committed
feat: strategy parameters support, parameter filter modal, dashboard improvements
- Add parameters field to Backtest dataclass (save/open/merge) - Add set_parameters()/get_parameters() to TradingStrategy - Wire parameters through backtest service and report builder - Add Parameters Comparison table on compare page - Add Strategy Parameters card on per-strategy pages - Add Parameters tab in strategy selection modal with chip/range filters - Fix buildCompareParameters to use selectedForCompare - Auto-apply param filters on modal close - Dashboard: metrics fixes, README updates
1 parent b1b67ab commit 495aaef

18 files changed

Lines changed: 2222 additions & 550 deletions

File tree

README.md

Lines changed: 126 additions & 143 deletions
Large diffs are not rendered by default.

_verify_fixes.py

Lines changed: 0 additions & 36 deletions
This file was deleted.

codecov.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

investing_algorithm_framework/app/reporting/backtest_report.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import csv
3+
import base64
34
import webbrowser
45
import logging
56
from dataclasses import dataclass, field
@@ -38,6 +39,18 @@ def _read_template(filename):
3839
return f.read()
3940

4041

42+
def _read_logo_base64(filename):
43+
logo_dir = os.path.join(
44+
os.path.dirname(__file__), '..', '..', 'domain',
45+
'backtesting', 'templates'
46+
)
47+
path = os.path.join(logo_dir, filename)
48+
if not os.path.exists(path):
49+
return ''
50+
with open(path, 'rb') as f:
51+
return base64.b64encode(f.read()).decode('ascii')
52+
53+
4154
def _filter_pct(v):
4255
"""Format as signed percentage: 0.15 → '+15.0%', None → 'N/A'."""
4356
if v is None:
@@ -174,6 +187,8 @@ def _build_html(self):
174187

175188
css = _read_template('dashboard.css')
176189
js = _read_template('dashboard.js')
190+
logo_dark_b64 = _read_logo_base64('finterion-dark.png')
191+
logo_light_b64 = _read_logo_base64('finterion-light.png')
177192

178193
is_single = len(self.backtests) == 1
179194
strategies = self._build_strategies_data()
@@ -237,6 +252,8 @@ def _build_html(self):
237252
title=title,
238253
css=css,
239254
js=js,
255+
logo_dark_b64=logo_dark_b64,
256+
logo_light_b64=logo_light_b64,
240257
is_single=is_single,
241258
strategies=strategies,
242259
run_data=run_data,
@@ -309,6 +326,7 @@ def _build_strategies_data(self):
309326
'runIds': run_ids,
310327
'runNameMap': run_name_map,
311328
'runLabels': run_labels_list,
329+
'parameters': bt.parameters or {},
312330
})
313331

314332
return strategies
@@ -434,21 +452,64 @@ def _build_run_data(self):
434452
'average_trade_duration',
435453
'average_win_duration',
436454
'average_loss_duration',
455+
'final_value',
456+
'gross_profit', 'gross_loss',
457+
'percentage_winning_months',
458+
'percentage_winning_years',
459+
'median_trade_return',
460+
'median_trade_return_percentage',
461+
'number_of_positive_trades',
462+
'number_of_negative_trades',
463+
'percentage_positive_trades',
464+
'percentage_negative_trades',
465+
'average_monthly_return',
466+
'var_95', 'cvar_95',
467+
'max_consecutive_wins',
468+
'max_consecutive_losses',
437469
):
438470
metrics_dict[attr] = getattr(m, attr, None)
439471

440-
# Snapshot
472+
# Serialize tuple metrics (value, date)
473+
for tattr in (
474+
'best_month', 'worst_month',
475+
'best_year', 'worst_year',
476+
):
477+
tval = getattr(m, tattr, None)
478+
if tval and tval[0] is not None:
479+
metrics_dict[tattr] = {
480+
'value': tval[0],
481+
'date': _fmt_date(tval[1]) if tval[1]
482+
else None,
483+
}
484+
485+
# Snapshot: raw portfolio values for Portfolio Summary
441486
snapshot = {}
442487
if run.portfolio_snapshots:
488+
first = run.portfolio_snapshots[0]
443489
last = run.portfolio_snapshots[-1]
444-
tv = getattr(last, 'total_value', 0) or 0
490+
first_val = getattr(first, 'total_value', 0) or 0
491+
last_val = getattr(last, 'total_value', 0) or 0
492+
net_gain_raw = last_val - first_val
493+
growth_pct = round(
494+
(last_val / first_val - 1) * 100, 2
495+
) if first_val else 0
445496
snapshot = {
446-
'equity': tv,
447-
'unallocated': getattr(last, 'unallocated', 0) or 0,
448-
'net_profit': getattr(last, 'total_net_gain', 0) or 0,
449-
'growth': round(
450-
(tv / initial - 1) * 100, 2
451-
) if initial else 0,
497+
'initial_value': round(first_val, 2),
498+
'final_value': round(last_val, 2),
499+
'net_gain': round(net_gain_raw, 2),
500+
'growth': growth_pct,
501+
'unallocated': round(
502+
getattr(last, 'unallocated', 0) or 0, 2
503+
),
504+
'total_net_gain': round(
505+
getattr(last, 'total_net_gain', 0) or 0, 2
506+
),
507+
'total_revenue': round(
508+
getattr(last, 'total_revenue', 0) or 0, 2
509+
),
510+
'total_cost': round(
511+
getattr(last, 'total_cost', 0) or 0, 2
512+
),
452513
}
453514

454515
run_data[rid] = {

investing_algorithm_framework/app/reporting/templates/dashboard.css

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color
2929
.top-controls { position:fixed; top:0.75rem; right:1.25rem; z-index:20; display:flex; gap:0.5rem; align-items:center; }
3030
.theme-toggle { background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:0.35rem 0.6rem; cursor:pointer; font-size:0.9rem; color:var(--text); }
3131
.theme-toggle:hover { background:var(--surface2); border-color:var(--accent); }
32+
.save-btn { background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:0.35rem 0.75rem; cursor:pointer; font-size:0.78rem; font-weight:600; color:var(--text); transition:all 0.15s; }
33+
.save-btn:hover { background:var(--surface2); border-color:var(--accent); }
3234
.finterion-tab { display:inline-flex; align-items:center; background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:0.35rem 0.75rem; cursor:pointer; transition:all 0.15s; }
3335
.finterion-tab:hover { border-color:var(--accent); }
3436
.finterion-tab.active { border-color:var(--accent); background:var(--accent); }
@@ -170,6 +172,31 @@ body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color
170172
.modal-body { overflow:auto; flex:1; padding:0; }
171173
.modal-footer { display:flex; align-items:center; justify-content:space-between; padding:0.75rem 1.25rem; border-top:1px solid var(--border); }
172174

175+
/* Modal tabs */
176+
.modal-tabs { display:flex; gap:2px; background:var(--surface2); border-radius:6px; padding:2px; }
177+
.modal-tab { background:none; border:none; padding:4px 12px; font-size:0.7rem; font-weight:600; color:var(--text-dim); cursor:pointer; border-radius:4px; transition:all 0.15s; text-transform:uppercase; letter-spacing:0.03em; }
178+
.modal-tab:hover { color:var(--text-secondary); }
179+
.modal-tab.active { background:var(--accent); color:#fff; }
180+
181+
/* Parameter filters */
182+
.param-filter-row { display:flex; align-items:center; gap:10px; padding:8px 16px; border-bottom:1px solid var(--border); font-size:0.75rem; }
183+
.param-filter-row:last-child { border-bottom:none; }
184+
.param-filter-label { font-weight:600; color:var(--text-secondary); min-width:140px; font-family:'JetBrains Mono',monospace; font-size:0.72rem; }
185+
.param-filter-controls { display:flex; align-items:center; gap:6px; flex:1; flex-wrap:wrap; }
186+
.param-filter-chip { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:12px; font-size:0.68rem; font-family:'JetBrains Mono',monospace; cursor:pointer; border:1px solid var(--border); background:var(--surface); color:var(--text-secondary); transition:all 0.15s; user-select:none; }
187+
.param-filter-chip:hover { border-color:var(--accent); color:var(--text); }
188+
.param-filter-chip.selected { background:var(--accent); color:#fff; border-color:var(--accent); }
189+
.param-filter-range { display:flex; align-items:center; gap:4px; }
190+
.param-filter-range input { width:70px; padding:3px 6px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); font-size:0.7rem; font-family:'JetBrains Mono',monospace; }
191+
.param-filter-range input:focus { outline:none; border-color:var(--accent); }
192+
.param-filter-range span { color:var(--text-dim); font-size:0.68rem; }
193+
.param-filter-match { margin-left:auto; font-size:0.68rem; color:var(--text-dim); white-space:nowrap; }
194+
.param-filter-actions { display:flex; align-items:center; gap:8px; padding:10px 16px; border-bottom:1px solid var(--border); }
195+
.param-filter-apply { padding:5px 14px; border-radius:6px; border:none; background:var(--accent); color:#fff; font-size:0.72rem; font-weight:600; cursor:pointer; }
196+
.param-filter-apply:hover { opacity:0.9; }
197+
.param-filter-reset { padding:5px 14px; border-radius:6px; border:1px solid var(--border); background:none; color:var(--text-secondary); font-size:0.72rem; cursor:pointer; }
198+
.param-filter-reset:hover { border-color:var(--text-dim); color:var(--text); }
199+
173200
/* heatmap */
174201
.heatmap-table { width:100%; border-collapse:collapse; font-size:0.72rem; font-family:'JetBrains Mono',monospace; }
175202
.heatmap-table th,.heatmap-table td { padding:0.35rem 0.5rem; text-align:center; border:1px solid var(--border); }
@@ -190,18 +217,42 @@ body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color
190217
.tooltip.visible { opacity:1; }
191218

192219
/* finterion page */
193-
.finterion-hero { display:flex; align-items:center; gap:1.25rem; margin-bottom:2rem; }
194-
.hero-logo svg { height:40px; width:auto; }
195220
.hero-logo-dark { display:block; }
196221
.hero-logo-light { display:none; }
197222
[data-theme="light"] .hero-logo-dark { display:none; }
198223
[data-theme="light"] .hero-logo-light { display:block; }
199-
.finterion-cta { display:inline-block; margin-top:1rem; padding:0.6rem 1.5rem; background:var(--accent); color:#000; font-weight:600; font-size:0.82rem; border-radius:8px; text-decoration:none; }
200-
.finterion-steps { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:1rem; }
201-
.finterion-step { background:var(--surface2); border:1px solid var(--border); border-radius:8px; padding:1rem; }
202-
.step-num { width:24px; height:24px; border-radius:50%; background:var(--accent); color:#000; font-weight:700; font-size:0.75rem; display:flex; align-items:center; justify-content:center; margin-bottom:0.5rem; }
203-
.finterion-step h4 { font-size:0.82rem; margin-bottom:0.3rem; }
204-
.finterion-step p { font-size:0.75rem; color:var(--text-secondary); line-height:1.5; }
224+
225+
/* hero */
226+
.fin-hero { text-align:center; padding:2.5rem 1.5rem 2rem; background:var(--surface); border:1px solid var(--border); border-radius:12px; margin-bottom:1.5rem; }
227+
.fin-hero-logo { margin-bottom:1rem; }
228+
.fin-hero-title { font-size:1.5rem; font-weight:700; margin:0 0 0.6rem; color:var(--text); }
229+
.fin-hero-sub { font-size:0.88rem; color:var(--text-secondary); line-height:1.7; max-width:560px; margin:0 auto; }
230+
.finterion-cta { display:inline-block; margin-top:1.25rem; padding:0.65rem 1.75rem; background:var(--accent); color:#000; font-weight:600; font-size:0.82rem; border-radius:8px; text-decoration:none; transition:opacity 0.15s; }
231+
.finterion-cta:hover { opacity:0.85; }
232+
233+
/* feature cards */
234+
.fin-features { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:1rem; margin-bottom:0.5rem; }
235+
.fin-feature { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:1.5rem 1.25rem; text-align:center; transition:border-color 0.15s; }
236+
.fin-feature:hover { border-color:var(--accent); }
237+
.fin-feature-icon { font-size:1.75rem; margin-bottom:0.75rem; }
238+
.fin-feature h4 { font-size:0.88rem; font-weight:600; margin-bottom:0.4rem; color:var(--text); }
239+
.fin-feature p { font-size:0.78rem; color:var(--text-secondary); line-height:1.6; }
240+
241+
/* timeline steps */
242+
.fin-timeline { display:flex; flex-direction:column; gap:0; padding:0.5rem 0; }
243+
.fin-tl-step { display:flex; align-items:flex-start; gap:1rem; position:relative; padding-bottom:1.5rem; }
244+
.fin-tl-step:last-child { padding-bottom:0; }
245+
.fin-tl-step::before { content:''; position:absolute; left:11px; top:28px; bottom:0; width:2px; background:var(--border); }
246+
.fin-tl-step:last-child::before { display:none; }
247+
.step-num { width:24px; height:24px; border-radius:50%; background:var(--accent); color:#000; font-weight:700; font-size:0.75rem; display:flex; align-items:center; justify-content:center; flex-shrink:0; position:relative; z-index:1; }
248+
.fin-tl-content { padding-top:2px; }
249+
.fin-tl-content h4 { font-size:0.85rem; font-weight:600; margin-bottom:0.25rem; color:var(--text); }
250+
.fin-tl-content p, .fin-tl-content code { font-size:0.78rem; color:var(--text-secondary); line-height:1.55; }
251+
.fin-tl-content code { display:inline-block; background:var(--surface2); border:1px solid var(--border); border-radius:4px; padding:0.2rem 0.5rem; font-family:'JetBrains Mono',monospace; font-size:0.72rem; margin-top:0.15rem; }
252+
253+
/* bottom cta */
254+
.fin-bottom-cta { text-align:center; padding:2rem 0 1rem; }
255+
.fin-bottom-cta p { font-size:0.85rem; color:var(--text-secondary); margin-bottom:0; }
205256
.dot { width:8px; height:8px; border-radius:50%; display:inline-block; }
206257

207258
/* gen-note */
@@ -222,6 +273,9 @@ body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color
222273
::-webkit-scrollbar-track { background:transparent; }
223274
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
224275

276+
/* Parameters table */
277+
.params-table td:first-child { font-weight:600; color:var(--text-secondary); }
278+
225279
@media (max-width:900px) {
226280
.sidebar { display:none; }
227281
.main { margin-left:0; padding:1.5rem 1rem 4rem; }

0 commit comments

Comments
 (0)