Skip to content

Commit ebafedd

Browse files
committed
feat: add orders/trades/positions tables and timeline scatter to backtest report
- Add ORDERS and POSITIONS data to run_data in BacktestReport - Add sortable tables for trades, orders, and positions per strategy - Add Trade & Order Timeline scatter chart (Chart.js) with buy/sell markers - Fix single-mode strategy pages (were dead code due to ID mismatch) - Fix NAType serialization in best_year/worst_year metrics - Unify strategy page rendering for both single and multi mode
1 parent 96095a1 commit ebafedd

3 files changed

Lines changed: 403 additions & 25 deletions

File tree

investing_algorithm_framework/app/reporting/backtest_report.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ def _fmt_date(dt):
7777
return dt.strftime('%Y-%m-%d')
7878

7979

80+
def _is_na(val):
81+
"""Check whether *val* is a pandas-like NA/NaN sentinel."""
82+
try:
83+
import pandas as pd
84+
if pd.isna(val):
85+
return True
86+
except (ImportError, TypeError, ValueError):
87+
pass
88+
return False
89+
90+
8091
@dataclass
8192
class BacktestReport:
8293
backtests: List[Backtest] = field(default_factory=list)
@@ -518,7 +529,11 @@ def _build_run_data(self):
518529
'best_year', 'worst_year',
519530
):
520531
tval = getattr(m, tattr, None)
521-
if tval and tval[0] is not None:
532+
if (
533+
tval
534+
and tval[0] is not None
535+
and not _is_na(tval[0])
536+
):
522537
metrics_dict[tattr] = {
523538
'value': tval[0],
524539
'date': _fmt_date(tval[1]) if tval[1]
@@ -555,6 +570,54 @@ def _build_run_data(self):
555570
),
556571
}
557572

573+
# Orders
574+
orders_list = []
575+
if run.orders:
576+
for o in run.orders:
577+
o_dt = getattr(o, 'created_at', None)
578+
u_dt = getattr(o, 'updated_at', None)
579+
orders_list.append({
580+
'sym': getattr(o, 'target_symbol', '')
581+
or '',
582+
'side': getattr(o, 'order_side', '')
583+
or '',
584+
'type': getattr(o, 'order_type', '')
585+
or '',
586+
'status': getattr(o, 'status', '')
587+
or '',
588+
'price': round(
589+
getattr(o, 'price', 0) or 0, 4
590+
),
591+
'amount': round(
592+
getattr(o, 'amount', 0) or 0, 6
593+
),
594+
'filled': round(
595+
getattr(o, 'filled', 0) or 0, 6
596+
),
597+
'cost': round(
598+
(getattr(o, 'amount', 0) or 0)
599+
* (getattr(o, 'price', 0) or 0), 2
600+
),
601+
'created': _fmt_date(o_dt)
602+
if o_dt else '',
603+
'updated': _fmt_date(u_dt)
604+
if u_dt else '',
605+
})
606+
607+
# Positions
608+
positions_list = []
609+
if run.positions:
610+
for p in run.positions:
611+
positions_list.append({
612+
'sym': getattr(p, 'symbol', '') or '',
613+
'amount': round(
614+
getattr(p, 'amount', 0) or 0, 6
615+
),
616+
'cost': round(
617+
getattr(p, 'cost', 0) or 0, 2
618+
),
619+
})
620+
558621
run_data[rid] = {
559622
'label': label,
560623
'EQ': eq,
@@ -565,6 +628,8 @@ def _build_run_data(self):
565628
'YR': yr,
566629
'MONTHLY_HEATMAP': heatmap,
567630
'TRADES': trades_list,
631+
'ORDERS': orders_list,
632+
'POSITIONS': positions_list,
568633
'SYM_STATS': sym_stats,
569634
'metrics': metrics_dict,
570635
'snapshot': snapshot,

0 commit comments

Comments
 (0)