Coverage for src/cc_liquid/cli_display.py: 0%

890 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-10-13 20:16 +0000

1"""Display utilities for rendering structured data.""" 

2 

3from datetime import datetime, timezone 

4from typing import TYPE_CHECKING, Any 

5 

6from rich import box 

7from rich.box import DOUBLE 

8from rich.columns import Columns 

9from rich.console import Console, Group 

10from rich.layout import Layout 

11from rich.panel import Panel 

12from rich.table import Table 

13from rich.text import Text 

14import re 

15 

16if TYPE_CHECKING: 

17 from .trader import AccountInfo, PortfolioInfo 

18 from .order import Order, OrderHistory 

19 from .stop_loss import StopLossOrder 

20 

21 

22def strip_ansi_codes(text: str) -> str: 

23 """Strip ANSI escape codes from text to prevent Rich layout issues.""" 

24 ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") 

25 return ansi_escape.sub("", text) 

26 

27 

28def create_setup_welcome_panel() -> Panel: 

29 """Build the setup wizard welcome panel used by init command.""" 

30 welcome_text = Text.from_markup( 

31 "[bold cyan]Welcome to cc-liquid setup![/bold cyan]\n\n" 

32 "This wizard will help you create:\n" 

33 "• [cyan].env[/cyan] - for your private keys (never commit!)\n" 

34 "• [cyan]cc-liquid-config.yaml[/cyan] - for your trading configuration\n\n" 

35 "[dim]Press Ctrl+C anytime to cancel[/dim]" 

36 ) 

37 return Panel(welcome_text, title="Setup Wizard", border_style="cyan") 

38 

39 

40def create_setup_summary_panel( 

41 is_testnet: bool, 

42 data_source: str, 

43 num_long: int, 

44 num_short: int, 

45 leverage: float, 

46) -> Panel: 

47 """Build the final setup summary panel used by init command.""" 

48 summary_text = Text.from_markup( 

49 f"[bold green]✅ Setup Complete![/bold green]\n\n" 

50 f"Environment: [cyan]{'TESTNET' if is_testnet else 'MAINNET'}[/cyan]\n" 

51 f"Data source: [cyan]{data_source}[/cyan]\n" 

52 f"Portfolio: [green]{num_long}L[/green] / [red]{num_short}S[/red] @ [yellow]{leverage}x[/yellow]\n\n" 

53 "[bold]Next steps:[/bold]\n" 

54 "1. Fill in any missing values in [cyan].env[/cyan]\n" 

55 "2. Test connection: [cyan]cc-liquid account[/cyan]\n" 

56 "3. View config: [cyan]cc-liquid config[/cyan]\n" 

57 "4. First rebalance: [cyan]cc-liquid rebalance[/cyan]\n\n" 

58 "[dim]Optional: Install tab completion with 'cc-liquid completion install'[/dim]" 

59 ) 

60 return Panel( 

61 summary_text, 

62 title="🎉 Ready to Trade", 

63 border_style="green", 

64 ) 

65 

66 

67def create_plotext_panel( 

68 plot_func, title: str, width: int = 50, height: int = 10, style: str = "cyan" 

69) -> Panel: 

70 """Create a Rich panel with plotext content, handling ANSI codes properly.""" 

71 import plotext as plt 

72 

73 # Clear and configure plotext 

74 plt.clf() 

75 plt.plotsize(width, height) 

76 

77 # Execute the plotting function 

78 plot_func(plt) 

79 

80 # Get clean output 

81 chart_str = plt.build() 

82 clean_chart = strip_ansi_codes(chart_str) 

83 

84 return Panel(Text(clean_chart, style=style), title=title, box=box.HEAVY) 

85 

86 

87def create_data_bar( 

88 value: float, 

89 max_value: float, 

90 width: int = 20, 

91 filled_char: str = "█", 

92 empty_char: str = "░", 

93) -> str: 

94 """Create a visual data bar for representing proportions.""" 

95 if max_value == 0: 

96 filled_width = 0 

97 else: 

98 filled_width = int((value / max_value) * width) 

99 empty_width = width - filled_width 

100 return f"{filled_char * filled_width}{empty_char * empty_width}" 

101 

102 

103def format_currency(value: float, compact: bool = False) -> str: 

104 """Format currency values with appropriate styling.""" 

105 if compact and abs(value) >= 1000: 

106 if abs(value) >= 1_000_000: 

107 return f"${value / 1_000_000:.1f}M" 

108 return f"${value / 1_000:.1f}K" 

109 return f"${value:,.2f}" 

110 

111 

112def create_metric_row(label: str, value: str, style: str = "") -> tuple: 

113 """Create a metric row tuple for tables.""" 

114 return (f"[cyan]{label}[/cyan]", f"[{style}]{value}[/{style}]" if style else value) 

115 

116 

117def create_header_panel(base_title: str, is_rebalancing: bool = False) -> Panel: 

118 """Create a header panel for the dashboard view, optionally showing rebalancing status.""" 

119 header_text = base_title 

120 if is_rebalancing: 

121 header_text += " :: [yellow blink]REBALANCING[/yellow blink]" 

122 return Panel( 

123 Text(header_text, style="bold cyan", justify="center"), box=DOUBLE, style="cyan" 

124 ) 

125 

126 

127def create_account_metrics_table(account: "AccountInfo") -> Table: 

128 """Create a compact account metrics table for the metrics Panel of the dashboard view.""" 

129 table = Table(show_header=False, box=None, padding=(0, 1)) 

130 table.add_column("", width=10) 

131 table.add_column("", justify="right") 

132 

133 # Leverage color based on risk 

134 lev_color = ( 

135 "green" 

136 if account.current_leverage <= 2 

137 else "yellow" 

138 if account.current_leverage <= 3 

139 else "red" 

140 ) 

141 

142 rows = [ 

143 create_metric_row( 

144 "VALUE", format_currency(account.account_value), "bold green" 

145 ), 

146 create_metric_row("MARGIN", format_currency(account.margin_used)), 

147 create_metric_row("FREE", format_currency(account.free_collateral)), 

148 create_metric_row("LEVERAGE", f"{account.current_leverage:.2f}x", lev_color), 

149 ] 

150 

151 for row in rows: 

152 table.add_row(*row) 

153 

154 return table 

155 

156 

157def create_account_exposure_table(portfolio: "PortfolioInfo") -> Table: 

158 """Create a compact exposure analysis table for the metrics Panel of the dashboard view.""" 

159 table = Table(show_header=False, box=None, padding=(0, 1)) 

160 table.add_column("", width=8) 

161 table.add_column("", justify="right", width=10) 

162 table.add_column("", width=18) 

163 

164 account_val = ( 

165 portfolio.account.account_value if portfolio.account.account_value > 0 else 1 

166 ) 

167 

168 # Calculate percentages 

169 long_pct = portfolio.total_long_value / account_val * 100 

170 short_pct = portfolio.total_short_value / account_val * 100 

171 net_pct = portfolio.net_exposure / account_val * 100 

172 gross_pct = long_pct + short_pct # Just sum the percentages! 

173 

174 # Build rows with visual bars for long/short 

175 net_color = "green" if portfolio.net_exposure >= 0 else "red" 

176 rows = [ 

177 ( 

178 "LONG", 

179 f"[green]{format_currency(portfolio.total_long_value, compact=True)}[/green]", 

180 f"[green]{create_data_bar(long_pct, 300, 12, '▓')} {long_pct:.0f}%[/green]", 

181 ), 

182 ( 

183 "SHORT", 

184 f"[red]{format_currency(portfolio.total_short_value, compact=True)}[/red]", 

185 f"[red]{create_data_bar(short_pct, 300, 12, '▓')} {short_pct:.0f}%[/red]", 

186 ), 

187 ( 

188 "NET", 

189 f"[{net_color}]{format_currency(portfolio.net_exposure, compact=True)}[/{net_color}]", 

190 f"[dim]{net_pct:+.0f}%[/dim]", 

191 ), 

192 ( 

193 "GROSS", 

194 format_currency(portfolio.total_exposure, compact=True), 

195 f"[dim]{gross_pct:.0f}%[/dim]", 

196 ), 

197 ] 

198 

199 for row in rows: 

200 table.add_row(*row) 

201 

202 return table 

203 

204 

205def create_metrics_panel(portfolio: "PortfolioInfo") -> Panel: 

206 """Create portfolio metrics panel (account + exposure) for the dashboard view.""" 

207 return Panel( 

208 Columns( 

209 [ 

210 create_account_metrics_table(portfolio.account), 

211 create_account_exposure_table(portfolio), 

212 ], 

213 expand=True, 

214 ), 

215 title="[bold cyan]METRICS[/bold cyan]", 

216 box=box.HEAVY, 

217 ) 

218 

219 

220def create_positions_panel(portfolio: "PortfolioInfo") -> Panel: 

221 """Create a panel displaying all open positions with summary statistics. 

222 

223 The panel includes a table of positions (sorted by value), and a title summarizing 

224 the number of longs/shorts and total unrealized PnL. 

225 

226 Args: 

227 portfolio (PortfolioInfo): The portfolio containing positions and account info. 

228 

229 Returns: 

230 Panel: A rich Panel containing the positions table and summary. 

231 """ 

232 positions = portfolio.positions 

233 

234 if not portfolio.positions: 

235 return Panel( 

236 "[yellow]No open positions[/yellow]", 

237 box=box.HEAVY, 

238 title="[bold cyan]POSITIONS[/bold cyan]", 

239 ) 

240 

241 account_val = ( 

242 portfolio.account.account_value if portfolio.account.account_value > 0 else 1 

243 ) 

244 long_count = sum(1 for p in positions if p.side == "LONG") 

245 short_count = sum(1 for p in positions if p.side == "SHORT") 

246 total_pnl = sum(p.unrealized_pnl for p in positions) 

247 pnl_pct = total_pnl / account_val * 100 

248 pnl_color = "green" if total_pnl >= 0 else "red" 

249 

250 title = ( 

251 f"[bold cyan]POSITIONS[/bold cyan] [dim]│[/dim] " 

252 f"[green]{long_count}L[/green] [red]{short_count}S[/red] [dim]│[/dim] " 

253 f"UNREALIZED [{pnl_color}]${total_pnl:+,.2f} ({pnl_pct:+.1f}%)[/{pnl_color}]" 

254 ) 

255 

256 table = Table( 

257 box=box.HEAVY_HEAD, 

258 show_lines=False, 

259 header_style="bold cyan on #001926", 

260 expand=True, 

261 ) 

262 

263 # Define columns 

264 table.add_column("COIN", style="cyan", width=8) 

265 table.add_column("SIDE", justify="center", width=8) 

266 table.add_column("SIZE", justify="right", width=8) 

267 table.add_column("ENTRY", justify="right", width=10) 

268 table.add_column("MARK", justify="right", width=10) 

269 table.add_column("VALUE", justify="right", width=12) 

270 table.add_column("PNL", justify="right", width=10) 

271 table.add_column("PERF", justify="center", width=8) 

272 

273 # Sort positions by absolute value 

274 sorted_positions = sorted(positions, key=lambda p: abs(p.value), reverse=True) 

275 

276 for pos in sorted_positions: 

277 side_style = "green" if pos.side == "LONG" else "red" 

278 pnl_color = "green" if pos.unrealized_pnl >= 0 else "red" 

279 

280 # Format size based on magnitude 

281 if abs(pos.size) >= 1000: 

282 size_str = f"{pos.size:,.0f}" 

283 elif abs(pos.size) >= 1: 

284 size_str = f"{pos.size:.2f}" 

285 else: 

286 size_str = f"{pos.size:.4f}" 

287 

288 table.add_row( 

289 f"[bold]{pos.coin}[/bold]", 

290 f"[{side_style}]{pos.side[:1]}[/{side_style}]", 

291 size_str, 

292 format_currency(pos.entry_price, compact=True), 

293 format_currency(pos.mark_price, compact=True), 

294 format_currency(abs(pos.value), compact=True), 

295 f"[{pnl_color}]${pos.unrealized_pnl:+,.2f}[/{pnl_color}]", 

296 f"[{pnl_color}]{pos.return_pct:+.1f}% [/{pnl_color}]", 

297 ) 

298 

299 return Panel(table, title=title, box=box.HEAVY) 

300 

301 

302def create_sidebar_panel(config_dict: dict | None, empty_label: str) -> Panel: 

303 """Create sidebar panel containing config or a standardized empty state.""" 

304 if config_dict: 

305 return create_config_panel(config_dict) 

306 return Panel(empty_label, box=box.HEAVY) 

307 

308 

309def create_footer_panel( 

310 next_rebalance_time: datetime | None, 

311 last_rebalance_time: datetime | None, 

312 refresh_seconds: float | None, 

313) -> Panel: 

314 """Create monitoring footer with countdown and status details.""" 

315 now = datetime.now(timezone.utc) 

316 

317 # Next rebalance countdown 

318 countdown_str = "" 

319 if next_rebalance_time: 

320 time_until = next_rebalance_time - now 

321 if time_until.total_seconds() > 0: 

322 total_seconds = int(time_until.total_seconds()) 

323 hours, remainder = divmod(total_seconds, 3600) 

324 minutes, seconds = divmod(remainder, 60) 

325 

326 if hours > 24: 

327 days = hours // 24 

328 hours = hours % 24 

329 countdown = f"{days}d {hours:02d}:{minutes:02d}:{seconds:02d}" 

330 else: 

331 countdown = f"{hours:02d}:{minutes:02d}:{seconds:02d}" 

332 

333 if total_seconds < 60: 

334 countdown_str = f"[bold yellow blink]{countdown}[/bold yellow blink]" 

335 elif total_seconds < 3600: 

336 countdown_str = f"[yellow]{countdown}[/yellow]" 

337 else: 

338 countdown_str = f"[green]{countdown}[/green]" 

339 else: 

340 countdown_str = "[bold red blink]REBALANCING[/bold red blink]" 

341 else: 

342 countdown_str = "[dim]Calculating...[/dim]" 

343 

344 # Last rebalance string 

345 last_rebalance_str = "[dim]Never[/dim]" 

346 if last_rebalance_time: 

347 time_since = now - last_rebalance_time 

348 hours_ago = time_since.total_seconds() / 3600 

349 if hours_ago < 24: 

350 last_rebalance_str = f"[dim]{hours_ago:.1f}h ago[/dim]" 

351 else: 

352 days_ago = hours_ago / 24 

353 last_rebalance_str = f"[dim]{days_ago:.1f}d ago[/dim]" 

354 

355 status_grid = Table.grid(expand=True) 

356 status_grid.add_column(justify="left") 

357 status_grid.add_column(justify="center") 

358 status_grid.add_column(justify="center") 

359 status_grid.add_column(justify="right") 

360 

361 status_grid.add_row( 

362 f"[bold cyan]Next rebalance: {countdown_str}[/bold cyan]", 

363 f"[dim]Last: {last_rebalance_str}[/dim]", 

364 f"[dim]Monitor refresh: {refresh_seconds:.1f}s[/dim]" 

365 if refresh_seconds is not None 

366 else "", 

367 "[red]Press Ctrl+C to exit[/red]", 

368 ) 

369 

370 return Panel(status_grid, box=box.HEAVY) 

371 

372 

373def create_dashboard_layout( 

374 portfolio: "PortfolioInfo", 

375 config_dict: dict | None = None, 

376 open_orders: list["Order"] | None = None, 

377 *, 

378 next_rebalance_time: datetime | None = None, 

379 last_rebalance_time: datetime | None = None, 

380 is_rebalancing: bool = False, 

381 refresh_seconds: float | None = None, 

382) -> Layout: 

383 """Unified portfolio dashboard builder. 

384 

385 - Builds the common header, body (metrics, positions, sidebar) 

386 - Optionally adds a monitoring footer when scheduling data is provided 

387 """ 

388 has_footer = any( 

389 value is not None 

390 for value in (next_rebalance_time, last_rebalance_time, refresh_seconds) 

391 ) 

392 

393 layout = Layout() 

394 

395 if has_footer: 

396 layout.split_column( 

397 Layout(name="header", size=3), 

398 Layout(name="body"), 

399 Layout(name="footer", size=3), 

400 ) 

401 else: 

402 layout.split_column(Layout(name="header", size=3), Layout(name="body")) 

403 

404 # Header 

405 header_title = ( 

406 "CC-LIQUID MONITOR :: METAMODEL REBALANCER" 

407 if has_footer 

408 else "CC-LIQUID :: METAMODEL REBALANCER" 

409 ) 

410 layout["header"].update( 

411 create_header_panel(header_title, is_rebalancing if has_footer else False) 

412 ) 

413 

414 # Body: split into main area and sidebar 

415 layout["body"].split_row( 

416 Layout(name="main", ratio=2), Layout(name="sidebar", ratio=1) 

417 ) 

418 

419 # Main area: metrics + positions (+ orders if present) 

420 if open_orders and len(open_orders) > 0: 

421 layout["main"].split_column( 

422 Layout(name="metrics", size=8), 

423 Layout(name="orders", size=10), 

424 Layout(name="positions") 

425 ) 

426 layout["orders"].update(create_open_orders_panel(open_orders)) 

427 else: 

428 layout["main"].split_column( 

429 Layout(name="metrics", size=8), Layout(name="positions") 

430 ) 

431 

432 layout["metrics"].update(create_metrics_panel(portfolio)) 

433 layout["positions"].update(create_positions_panel(portfolio)) 

434 

435 # Sidebar 

436 empty_sidebar_text = ( 

437 "[dim]No config loaded[/dim]" if has_footer else "[dim]No config[/dim]" 

438 ) 

439 layout["sidebar"].update(create_sidebar_panel(config_dict, empty_sidebar_text)) 

440 

441 # Footer (optional) 

442 if has_footer: 

443 footer = create_footer_panel( 

444 next_rebalance_time, last_rebalance_time, refresh_seconds 

445 ) 

446 layout["footer"].update(footer) 

447 

448 return layout 

449 

450 

451def create_config_tree_table(config_dict: dict) -> Table: 

452 """Create config display as a tree structure (reusable).""" 

453 table = Table(show_header=False, box=None, padding=(0, 0)) 

454 table.add_column("Setting", style="cyan", width=20, no_wrap=True) 

455 table.add_column("Value", style="white") 

456 

457 # Environment section 

458 network = "TESTNET" if config_dict.get("is_testnet", False) else "MAINNET" 

459 network_color = "yellow" if config_dict.get("is_testnet", False) else "green" 

460 

461 # Data source section 

462 data_config = config_dict.get("data", {}) 

463 source = data_config.get("source", "crowdcent") 

464 source_color = ( 

465 "green" 

466 if source == "crowdcent" 

467 else "yellow" 

468 if source == "numerai" 

469 else "white" 

470 ) 

471 

472 # Portfolio section 

473 portfolio_config = config_dict.get("portfolio", {}) 

474 leverage = portfolio_config.get("target_leverage", 1.0) 

475 leverage_color = "green" if leverage <= 2 else "yellow" if leverage <= 3 else "red" 

476 rebalancing = portfolio_config.get("rebalancing", {}) 

477 weighting_scheme = portfolio_config.get("weighting_scheme", "equal") 

478 rank_power = portfolio_config.get("rank_power", 1.5) 

479 

480 # Execution section 

481 execution_config = config_dict.get("execution", {}) 

482 slippage_pct = execution_config.get("slippage_tolerance", 0.005) * 100 

483 slippage_color = ( 

484 "green" if slippage_pct <= 0.5 else "yellow" if slippage_pct <= 1.0 else "red" 

485 ) 

486 min_trade_value = execution_config.get("min_trade_value", 10.0) 

487 

488 # Profile section (owner/vault and signer env name) 

489 profile_cfg = config_dict.get("profile", {}) 

490 owner = profile_cfg.get("owner") or "[dim]-[/dim]" 

491 vault = profile_cfg.get("vault") or "[dim]-[/dim]" 

492 active_profile = profile_cfg.get("active") or "[dim]-[/dim]" 

493 signer_env = profile_cfg.get("signer_env") or "HYPERLIQUID_PRIVATE_KEY" 

494 

495 # Build all rows 

496 rows = [ 

497 ("[bold]ENVIRONMENT[/bold]", ""), 

498 ("├─ Network", f"[{network_color}]{network}[/{network_color}]"), 

499 ("├─ Active Profile", f"[white]{active_profile}[/white]"), 

500 ("├─ Owner", f"[white]{owner}[/white]"), 

501 ("├─ Vault", f"[white]{vault}[/white]"), 

502 ("└─ Signer Env", f"[white]{signer_env}[/white]"), 

503 ("", ""), 

504 ("[bold]DATA SOURCE[/bold]", ""), 

505 ("├─ Provider", f"[{source_color}]{source}[/{source_color}]"), 

506 ("├─ Path", data_config.get("path", "predictions.parquet")), 

507 ("└─ Prediction", data_config.get("prediction_column", "pred_10d")), 

508 ("", ""), 

509 ("[bold]PORTFOLIO[/bold]", ""), 

510 ("├─ Long Positions", f"[green]{portfolio_config.get('num_long', 10)}[/green]"), 

511 ("├─ Short Positions", f"[red]{portfolio_config.get('num_short', 10)}[/red]"), 

512 ("├─ Target Leverage", f"[{leverage_color}]{leverage:.1f}x[/{leverage_color}]"), 

513 ( 

514 "├─ Weighting", 

515 f"{weighting_scheme} ({rank_power})" 

516 if weighting_scheme == "rank_power" 

517 else "", 

518 ), 

519 ("└─ Rebalancing", ""), 

520 (" ├─ Frequency", f"Every {rebalancing.get('every_n_days', 10)} days"), 

521 (" └─ Time (UTC)", rebalancing.get("at_time", "18:15")), 

522 ("", ""), 

523 ("[bold]EXECUTION[/bold]", ""), 

524 ("├─ Slippage", f"[{slippage_color}]{slippage_pct:.1f}%[/{slippage_color}]"), 

525 ("└─ Min Trade Value", format_currency(min_trade_value, compact=False)), 

526 ] 

527 

528 for row in rows: 

529 table.add_row(*row) 

530 

531 return table 

532 

533 

534def display_portfolio( 

535 portfolio: "PortfolioInfo", 

536 console: Console | None = None, 

537 config_dict: dict | None = None, 

538) -> None: 

539 """Display portfolio information in a compact dashboard.""" 

540 if console is None: 

541 console = Console() 

542 

543 # Use the new dashboard layout 

544 layout = create_dashboard_layout(portfolio, config_dict) 

545 console.print(layout) 

546 

547 

548def create_config_panel(config_dict: dict) -> Panel: 

549 """Create display Panel for the config view. Can be used standalone or as part of the dashboard view.""" 

550 return Panel( 

551 create_config_tree_table(config_dict), 

552 title="[bold cyan]CONFIG[/bold cyan]", 

553 box=box.HEAVY, 

554 ) 

555 

556 

557def display_file_summary( 

558 console: Console, predictions, output_path: str, model_name: str 

559) -> None: 

560 """Display a summary of downloaded predictions file.""" 

561 console.print(f"[green]✓[/green] Downloaded {model_name} to {output_path}") 

562 console.print(f"[cyan]Shape:[/cyan] {predictions.shape}") 

563 console.print(f"[cyan]Columns:[/cyan] {list(predictions.columns)}") 

564 

565 

566def show_pre_alpha_warning() -> None: 

567 """Display pre-alpha warning to users.""" 

568 console = Console() 

569 warning_copy = """ 

570This is pre-alpha software provided as a reference implementation only. 

571• Using this software may result in COMPLETE LOSS of funds. 

572• CrowdCent makes NO WARRANTIES and assumes NO LIABILITY for any losses. 

573• Users must comply with all Hyperliquid and CrowdCent terms of service. 

574• We do NOT endorse any vaults or strategies using this tool. 

575 

576[bold yellow]By continuing, you acknowledge that you understand and accept ALL risks.[/bold yellow] 

577 """ 

578 warning_text = Text.from_markup(warning_copy, justify="left") 

579 panel = Panel( 

580 warning_text, 

581 title="[bold cyan]CC-LIQUID ::[/bold cyan] [bold red] PRE-ALPHA SOFTWARE - USE AT YOUR OWN RISK [/bold red]", 

582 border_style="red", 

583 box=box.HEAVY, 

584 ) 

585 console.print(panel) 

586 

587 

588def show_rebalancing_plan( 

589 console: Console, 

590 target_positions: dict, 

591 trades: list, 

592 account_value: float, 

593 leverage: float, 

594) -> None: 

595 """Create a comprehensive rebalancing dashboard layout.""" 

596 # Header 

597 header = Panel( 

598 Text("REBALANCING PLAN", style="bold cyan", justify="center"), 

599 box=DOUBLE, 

600 style="cyan", 

601 ) 

602 console.print(header) 

603 

604 # Metrics row: account + rebalancing summary 

605 metrics_content = create_rebalancing_metrics_panel( 

606 account_value, leverage, trades, target_positions 

607 ) 

608 console.print(metrics_content) 

609 

610 # Trades panel 

611 trades_panel = create_trades_panel(trades) 

612 console.print(trades_panel) 

613 

614 # Check if we have skipped trades 

615 skipped_count = sum(1 for t in trades if t.get("skipped", False)) 

616 if skipped_count > 0: 

617 console.print( 

618 f"\n[bold yellow]⚠️ WARNING: {skipped_count} trade(s) marked as SKIPPED[/bold yellow]\n" 

619 f"[yellow]These positions cannot be resized due to minimum trade size constraints.[/yellow]\n" 

620 f"[yellow]They will remain at their current sizes, causing portfolio imbalance.[/yellow]\n" 

621 f"[dim]Consider: increasing account value, using higher leverage, or reducing position count.[/dim]" 

622 ) 

623 

624 

625def create_rebalancing_metrics_panel( 

626 account_value: float, leverage: float, trades: list, target_positions: dict 

627) -> Panel: 

628 """Create rebalancing metrics panel.""" 

629 # Position counts using the type field 

630 executable_trades = [t for t in trades if not t.get("skipped", False)] 

631 opens = sum(1 for t in executable_trades if t.get("type") == "open") 

632 closes = sum(1 for t in executable_trades if t.get("type") == "close") 

633 flips = sum(1 for t in executable_trades if t.get("type") == "flip") 

634 reduces = sum(1 for t in executable_trades if t.get("type") == "reduce") 

635 increases = sum(1 for t in executable_trades if t.get("type") == "increase") 

636 

637 # Target portfolio metrics 

638 total_long_value = sum(v for v in target_positions.values() if v > 0) 

639 total_short_value = abs(sum(v for v in target_positions.values() if v < 0)) 

640 

641 # Create two columns 

642 left_table = Table(show_header=False, box=None, padding=(0, 1)) 

643 left_table.add_column("", width=12) 

644 left_table.add_column("", justify="right") 

645 

646 left_table.add_row("ACCOUNT", format_currency(account_value, compact=False)) 

647 left_table.add_row( 

648 "LEVERAGE", 

649 f"[{'green' if leverage <= 2 else 'yellow' if leverage <= 3 else 'red'}]{leverage:.1f}x[/{'green' if leverage <= 2 else 'yellow' if leverage <= 3 else 'red'}]", 

650 ) 

651 left_table.add_row("MAX EXPOSURE", format_currency(account_value * leverage)) 

652 left_table.add_row("", "") # spacer 

653 left_table.add_row( 

654 "TARGET LONG", 

655 f"[green]{format_currency(total_long_value, compact=True)}[/green]", 

656 ) 

657 left_table.add_row( 

658 "TARGET SHORT", f"[red]{format_currency(total_short_value, compact=True)}[/red]" 

659 ) 

660 

661 right_table = Table(show_header=False, box=None, padding=(0, 1)) 

662 right_table.add_column("", width=12) 

663 right_table.add_column("", justify="right") 

664 

665 right_table.add_row("TRADES", f"[bold]{len(executable_trades)}[/bold]") 

666 if opens > 0: 

667 right_table.add_row("OPEN", f"[green]{opens}[/green]") 

668 if closes > 0: 

669 right_table.add_row("CLOSE", f"[red]{closes}[/red]") 

670 if flips > 0: 

671 right_table.add_row("FLIP", f"[yellow]{flips}[/yellow]") 

672 if reduces > 0: 

673 right_table.add_row("REDUCE", f"[blue]{reduces}[/blue]") 

674 if increases > 0: 

675 right_table.add_row("ADD", f"[cyan]{increases}[/cyan]") 

676 

677 return Panel( 

678 Columns([left_table, right_table], expand=True), 

679 title="[bold cyan]METRICS[/bold cyan]", 

680 box=box.HEAVY, 

681 ) 

682 

683 

684def create_trades_panel(trades: list) -> Panel: 

685 """Create a panel for the trades table matching the positions table style.""" 

686 # Handle no trades 

687 if not trades: 

688 return Panel( 

689 "[yellow]No trades required - portfolio is already balanced[/yellow]", 

690 box=box.HEAVY, 

691 title="[bold cyan]TRADES[/bold cyan]", 

692 ) 

693 

694 # Separate executable and skipped trades 

695 executable_trades = [t for t in trades if not t.get("skipped", False)] 

696 skipped_trades = [t for t in trades if t.get("skipped", False)] 

697 

698 # Calculate summary for title (only executable trades) 

699 total_volume = sum(abs(t.get("delta_value", 0)) for t in executable_trades) 

700 buy_count = sum(1 for t in executable_trades if t.get("is_buy")) 

701 sell_count = len(executable_trades) - buy_count 

702 buy_volume = sum( 

703 abs(t.get("delta_value", 0)) for t in executable_trades if t.get("is_buy") 

704 ) 

705 sell_volume = sum( 

706 abs(t.get("delta_value", 0)) for t in executable_trades if not t.get("is_buy") 

707 ) 

708 

709 # Add skipped count to title if any 

710 skipped_info = "" 

711 if skipped_trades: 

712 skipped_info = f" [dim]│[/dim] [yellow]{len(skipped_trades)} SKIPPED[/yellow]" 

713 

714 title = ( 

715 f"[bold cyan]TRADES[/bold cyan] [dim]│[/dim] " 

716 f"[green]{buy_count} BUY (${buy_volume:,.2f})[/green] " 

717 f"[red]{sell_count} SELL (${sell_volume:,.2f})[/red] [dim]│[/dim] " 

718 f"VOLUME [bold]${total_volume:,.2f}[/bold]" 

719 f"{skipped_info}" 

720 ) 

721 

722 table = Table( 

723 box=box.HEAVY_HEAD, 

724 show_lines=False, 

725 header_style="bold cyan on #001926", 

726 expand=True, 

727 ) 

728 

729 # Define columns 

730 table.add_column("COIN", style="cyan", width=8) 

731 table.add_column("ACTION", justify="center", width=7) 

732 table.add_column("CURRENT", justify="right", width=10) 

733 table.add_column("→", justify="center", width=1, style="dim") 

734 table.add_column("TARGET", justify="right", width=10) 

735 table.add_column("DELTA", justify="right", width=10) 

736 table.add_column("TRADE", justify="center", width=6) 

737 table.add_column("SIZE", justify="right", width=10) 

738 table.add_column("PRICE", justify="right", width=10) 

739 

740 # Sort trades by absolute delta value, with executable trades first 

741 sorted_trades = sorted( 

742 trades, key=lambda t: (t.get("skipped", False), -abs(t.get("delta_value", 0))) 

743 ) 

744 

745 for trade in sorted_trades: 

746 coin = trade["coin"] 

747 is_buy = trade.get("is_buy", False) # Skipped trades may not have this 

748 current_value = trade.get("current_value", 0) 

749 target_value = trade.get("target_value", 0) 

750 delta_value = trade.get("delta_value", 0) 

751 

752 # Use the type field from trade calculation 

753 trade_type = trade.get("type", "increase") # fallback for old data 

754 action_styles = { 

755 "open": "[green]OPEN[/green]", 

756 "close": "[red]CLOSE[/red]", 

757 "flip": "[yellow]FLIP[/yellow]", 

758 "reduce": "[blue]REDUCE[/blue]", 

759 "increase": "[cyan]ADD[/cyan]", 

760 } 

761 action = action_styles.get(trade_type, "[dim]ADJUST[/dim]") 

762 

763 # Format current and target with side indicators 

764 if current_value == 0: 

765 current_str = "[dim]-[/dim]" 

766 else: 

767 side = "L" if current_value > 0 else "S" 

768 side_color = "green" if current_value > 0 else "red" 

769 current_str = f"{format_currency(abs(current_value), compact=True)} [{side_color}]{side}[/{side_color}]" 

770 

771 if target_value == 0: 

772 target_str = "[dim]-[/dim]" 

773 else: 

774 side = "L" if target_value > 0 else "S" 

775 side_color = "green" if target_value > 0 else "red" 

776 target_str = f"{format_currency(abs(target_value), compact=True)} [{side_color}]{side}[/{side_color}]" 

777 

778 # Trade direction 

779 trade_action = "[green]BUY[/green]" if is_buy else "[red]SELL[/red]" 

780 

781 # Delta with color 

782 delta_color = "green" if delta_value > 0 else "red" 

783 delta_str = f"[{delta_color}]{delta_value:+,.0f}[/{delta_color}]" 

784 

785 # Style differently if trade is skipped 

786 if trade.get("skipped", False): 

787 # Show the skip reason in the trade column 

788 trade_action = "[yellow]SKIP[/yellow]" 

789 # Dim the entire row for skipped trades 

790 coin_str = f"[dim]{coin}[/dim]" 

791 action = f"[dim]{action}[/dim]" 

792 current_str = f"[dim]{current_str}[/dim]" 

793 target_str = f"[dim]{target_str}[/dim]" 

794 delta_str = f"[dim yellow]{delta_value:+,.0f}[/dim yellow]" 

795 size_str = "[dim]-[/dim]" # No size since it won't execute 

796 price_str = "[dim]-[/dim]" # No price since it won't execute 

797 else: 

798 coin_str = f"[bold]{coin}[/bold]" 

799 size_str = f"{trade.get('sz', 0):.4f}" if "sz" in trade else "[dim]-[/dim]" 

800 price_str = ( 

801 format_currency(trade["price"], compact=True) 

802 if "price" in trade 

803 else "[dim]-[/dim]" 

804 ) 

805 

806 table.add_row( 

807 coin_str, 

808 action, 

809 current_str, 

810 "→", 

811 target_str, 

812 delta_str, 

813 trade_action, 

814 size_str, 

815 price_str, 

816 ) 

817 

818 return Panel(table, title=title, box=box.HEAVY, expand=True) 

819 

820 

821def create_execution_metrics_panel( 

822 successful_trades: list[dict], 

823 all_trades: list[dict], 

824 target_positions: dict, 

825 account_value: float, 

826) -> Panel: 

827 """Create execution summary metrics panel.""" 

828 total_success = len(successful_trades) 

829 total_failed = len(all_trades) - total_success 

830 

831 # Calculate portfolio metrics 

832 total_long_value = sum(v for v in target_positions.values() if v > 0) 

833 total_short_value = abs(sum(v for v in target_positions.values() if v < 0)) 

834 total_exposure = total_long_value + total_short_value 

835 net_exposure = total_long_value - total_short_value 

836 leverage = total_exposure / account_value if account_value > 0 else 0 

837 

838 # Calculate slippage stats from successful trades 

839 if successful_trades: 

840 slippages = [t.get("slippage_pct", 0) for t in successful_trades] 

841 avg_slippage = sum(slippages) / len(slippages) 

842 max_slippage = max(slippages) 

843 min_slippage = min(slippages) 

844 else: 

845 avg_slippage = max_slippage = min_slippage = 0 

846 

847 # Create two columns 

848 left_table = Table(show_header=False, box=None, padding=(0, 1)) 

849 left_table.add_column("", width=15) 

850 left_table.add_column("", justify="right") 

851 

852 # Execution results 

853 left_table.add_row( 

854 "EXECUTED", f"[green]{total_success}[/green]" if total_success > 0 else "0" 

855 ) 

856 if total_failed > 0: 

857 left_table.add_row("FAILED", f"[red]{total_failed}[/red]") 

858 left_table.add_row( 

859 "SUCCESS RATE", 

860 f"[bold]{total_success / len(all_trades) * 100:.1f}%[/bold]" 

861 if all_trades 

862 else "N/A", 

863 ) 

864 left_table.add_row("", "") # spacer 

865 

866 # Slippage stats 

867 if successful_trades: 

868 left_table.add_row( 

869 "AVG SLIPPAGE", 

870 f"[{'green' if avg_slippage <= 0 else 'red'}]{avg_slippage:+.3f}%[/{'green' if avg_slippage <= 0 else 'red'}]", 

871 ) 

872 left_table.add_row("MAX SLIPPAGE", f"{max_slippage:+.3f}%") 

873 left_table.add_row("MIN SLIPPAGE", f"{min_slippage:+.3f}%") 

874 

875 right_table = Table(show_header=False, box=None, padding=(0, 1)) 

876 right_table.add_column("", width=15) 

877 right_table.add_column("", justify="right") 

878 

879 # Portfolio metrics 

880 right_table.add_row("TOTAL EXPOSURE", format_currency(total_exposure)) 

881 right_table.add_row("NET EXPOSURE", format_currency(net_exposure)) 

882 right_table.add_row( 

883 "LEVERAGE", 

884 f"[{'green' if leverage <= 2 else 'yellow' if leverage <= 3 else 'red'}]{leverage:.2f}x[/{'green' if leverage <= 2 else 'yellow' if leverage <= 3 else 'red'}]", 

885 ) 

886 right_table.add_row("", "") # spacer 

887 right_table.add_row( 

888 "LONG VALUE", 

889 f"[green]{format_currency(total_long_value, compact=True)}[/green]", 

890 ) 

891 right_table.add_row( 

892 "SHORT VALUE", f"[red]{format_currency(total_short_value, compact=True)}[/red]" 

893 ) 

894 right_table.add_row( 

895 "NET % OF NAV", 

896 f"{net_exposure / account_value * 100:+.1f}%" if account_value > 0 else "N/A", 

897 ) 

898 

899 return Panel( 

900 Columns([left_table, right_table], expand=True), 

901 title="[bold cyan]METRICS[/bold cyan]", 

902 box=box.HEAVY, 

903 ) 

904 

905 

906def create_execution_details_panel( 

907 successful_trades: list[dict], all_trades: list[dict] 

908) -> Panel: 

909 """Create execution details table.""" 

910 # Create a set of successful trade identifiers (coin + is_buy combination) 

911 successful_ids = {(t["coin"], t["is_buy"]) for t in successful_trades} 

912 failed_trades = [ 

913 t for t in all_trades if (t["coin"], t["is_buy"]) not in successful_ids 

914 ] 

915 

916 # Calculate volumes for successful trades 

917 success_volume = sum( 

918 float(t.get("fill_data", {}).get("totalSz", 0)) 

919 * float(t.get("fill_data", {}).get("avgPx", 0)) 

920 for t in successful_trades 

921 if "fill_data" in t 

922 ) 

923 

924 title = ( 

925 f"[bold cyan]EXECUTION DETAILS[/bold cyan] [dim]│[/dim] " 

926 f"[green]{len(successful_trades)} SUCCESS (${success_volume:,.2f})[/green] " 

927 f"[red]{len(failed_trades)} FAILED[/red]" 

928 ) 

929 

930 table = Table( 

931 box=box.HEAVY_HEAD, 

932 show_lines=False, 

933 header_style="bold cyan on #001926", 

934 title_style="bold cyan", 

935 ) 

936 

937 # Define columns 

938 table.add_column("COIN", style="cyan", width=8) 

939 table.add_column("SIDE", justify="center", width=6) 

940 table.add_column("SIZE", justify="right", width=10) 

941 table.add_column("EXPECTED", justify="right", width=10) 

942 table.add_column("FILLED", justify="right", width=10) 

943 table.add_column("SLIPPAGE", justify="right", width=10) 

944 table.add_column("VALUE", justify="right", width=12) 

945 table.add_column("STATUS", justify="center", width=8) 

946 

947 # Add successful trades first 

948 for trade in successful_trades: 

949 if "fill_data" in trade: 

950 fill = trade["fill_data"] 

951 side = "BUY" if trade["is_buy"] else "SELL" 

952 side_style = "green" if side == "BUY" else "red" 

953 slippage_style = "green" if trade.get("slippage_pct", 0) <= 0 else "red" 

954 

955 table.add_row( 

956 f"[bold]{trade['coin']}[/bold]", 

957 f"[{side_style}]{side}[/{side_style}]", 

958 f"{float(fill['totalSz']):.4f}", 

959 format_currency(trade["price"], compact=True), 

960 format_currency(float(fill["avgPx"]), compact=True), 

961 f"[{slippage_style}]{trade.get('slippage_pct', 0):+.3f}%[/{slippage_style}]", 

962 format_currency(float(fill["totalSz"]) * float(fill["avgPx"])), 

963 "[green]✓[/green]", 

964 ) 

965 

966 # Add failed trades 

967 for trade in failed_trades: 

968 side = "BUY" if trade["is_buy"] else "SELL" 

969 side_style = "green" if side == "BUY" else "red" 

970 

971 table.add_row( 

972 f"[bold]{trade['coin']}[/bold]", 

973 f"[{side_style}]{side}[/{side_style}]", 

974 f"{trade['sz']:.4f}", 

975 format_currency(trade["price"], compact=True), 

976 "[red]-[/red]", 

977 "[red]-[/red]", 

978 "[red]-[/red]", 

979 "[red]✗[/red]", 

980 ) 

981 

982 panel = Panel(table, title=title, box=box.HEAVY) 

983 return panel 

984 

985 

986def display_execution_summary( 

987 console: Console, 

988 successful_trades: list[dict], 

989 all_trades: list[dict], 

990 target_positions: dict, 

991 account_value: float, 

992) -> None: 

993 """Display execution summary after trades complete. 

994 

995 Prints panels sequentially (header → summary metrics → details), 

996 matching the style of the trade plan output. 

997 """ 

998 # Header 

999 header = Panel( 

1000 Text("EXECUTION SUMMARY", style="bold cyan", justify="center"), 

1001 box=DOUBLE, 

1002 style="cyan", 

1003 ) 

1004 console.print("\n") 

1005 console.print(header) 

1006 

1007 # Summary metrics 

1008 summary_panel = create_execution_metrics_panel( 

1009 successful_trades, all_trades, target_positions, account_value 

1010 ) 

1011 console.print(summary_panel) 

1012 

1013 # Details (only when there were any trades or failures) 

1014 if successful_trades or (len(all_trades) > len(successful_trades)): 

1015 details_panel = create_execution_details_panel(successful_trades, all_trades) 

1016 console.print(details_panel) 

1017 else: 

1018 console.print(Panel("[dim]No trades executed[/dim]", box=box.HEAVY)) 

1019 

1020 

1021def display_backtest_summary( 

1022 console: Console, result, config=None, show_positions=False 

1023): 

1024 """Display comprehensive backtest results in a cleaner sequential layout 

1025 

1026 Args: 

1027 console: Rich console for output 

1028 result: BacktestResult with daily data and stats 

1029 config: Optional BacktestConfig to display 

1030 show_positions: Whether to show the detailed position analysis table 

1031 """ 

1032 

1033 # 0. Disclaimer warning 

1034 disclaimer = Panel( 

1035 Text.from_markup( 

1036 "[yellow]\n Past performance does not guarantee future results. " 

1037 "These results are hypothetical and subject to limitations.[/yellow]\n", 

1038 justify="center", 

1039 ), 

1040 title="[bold yellow] BACKTEST DISCLAIMER [/bold yellow]", 

1041 box=box.HEAVY, 

1042 border_style="yellow", 

1043 ) 

1044 console.print(disclaimer) 

1045 

1046 # 1. Header 

1047 header = Panel( 

1048 Text( 

1049 "BACKTEST RESULTS :: PERFORMANCE ANALYSIS", 

1050 style="bold cyan", 

1051 justify="center", 

1052 ), 

1053 box=DOUBLE, 

1054 style="cyan", 

1055 ) 

1056 console.print(header) 

1057 

1058 # 2-3. Metrics + Charts stacked on the left, Config as a persistent right sidebar 

1059 metrics_panel = create_backtest_metrics_panel(result.stats) 

1060 

1061 if len(result.daily) > 0: 

1062 equity_panel = create_linechart_panel( 

1063 result.daily, "equity", "cyan", "Equity ($)" 

1064 ) 

1065 drawdown_panel = create_linechart_panel( 

1066 result.daily, "drawdown", "red", "Drawdown (%)" 

1067 ) 

1068 dist_panel = create_backtest_distributions(result.daily) 

1069 else: 

1070 equity_panel = Panel("[dim]No data[/dim]", box=box.HEAVY) 

1071 drawdown_panel = Panel("[dim]No data[/dim]", box=box.HEAVY) 

1072 dist_panel = Panel("[dim]No distribution data[/dim]", box=box.HEAVY) 

1073 

1074 top_row = Columns([equity_panel, drawdown_panel], expand=True) 

1075 left_group = Group(metrics_panel, top_row, dist_panel) 

1076 

1077 summary_layout = Layout() 

1078 summary_layout.split_row( 

1079 Layout(name="main", ratio=3), Layout(name="config", ratio=1) 

1080 ) 

1081 

1082 summary_layout["main"].update(left_group) 

1083 summary_layout["config"].update( 

1084 create_backtest_config_panel(config) 

1085 if config 

1086 else Panel( 

1087 "[dim]No configuration data[/dim]", 

1088 box=box.HEAVY, 

1089 title="[bold cyan]BACKTEST CONFIG[/bold cyan]", 

1090 ) 

1091 ) 

1092 

1093 console.print(summary_layout) 

1094 

1095 # 4. Positions table (full width) - only if flag is set 

1096 if show_positions and len(result.rebalance_positions) > 0: 

1097 positions_table = create_enhanced_positions_table( 

1098 result.rebalance_positions, result.daily 

1099 ) 

1100 console.print(positions_table) 

1101 

1102 

1103def create_backtest_config_panel(config) -> Panel: 

1104 """Create configuration panel for backtest sidebar using tree-like layout.""" 

1105 tree = Table(show_header=False, box=None, padding=(0, 0)) 

1106 tree.add_column("Setting", style="cyan", width=18, no_wrap=True) 

1107 tree.add_column("Value", style="white") 

1108 

1109 # Environment/date range 

1110 if config.start_date or config.end_date: 

1111 start_str = ( 

1112 config.start_date.strftime("%Y-%m-%d") if config.start_date else "start" 

1113 ) 

1114 end_str = config.end_date.strftime("%Y-%m-%d") if config.end_date else "end" 

1115 tree.add_row("[bold]RANGE[/bold]", "") 

1116 tree.add_row("├─ From", start_str) 

1117 tree.add_row("└─ To", end_str) 

1118 tree.add_row("", "") 

1119 

1120 # Portfolio 

1121 leverage_color = ( 

1122 "green" 

1123 if config.target_leverage <= 2 

1124 else "yellow" 

1125 if config.target_leverage <= 3 

1126 else "red" 

1127 ) 

1128 tree.add_row("[bold]PORTFOLIO[/bold]", "") 

1129 tree.add_row("├─ Long", f"[green]{config.num_long}[/green]") 

1130 tree.add_row("├─ Short", f"[red]{config.num_short}[/red]") 

1131 tree.add_row( 

1132 "├─ Leverage", 

1133 f"[{leverage_color}]{config.target_leverage:.1f}x[/{leverage_color}]", 

1134 ) 

1135 tree.add_row("└─ Rebalance", f"{config.rebalance_every_n_days}d") 

1136 tree.add_row("", "") 

1137 

1138 # Costs 

1139 tree.add_row("[bold]COSTS[/bold]", "") 

1140 tree.add_row("├─ Fee", f"{config.fee_bps:.1f}bps") 

1141 tree.add_row("└─ Slippage", f"{config.slippage_bps:.1f}bps") 

1142 tree.add_row("", "") 

1143 

1144 # Data 

1145 source_name = ( 

1146 config.predictions_path.split("/")[-1] 

1147 if "/" in config.predictions_path 

1148 else config.predictions_path 

1149 ) 

1150 provider = getattr(config, "data_provider", None) 

1151 provider_color = ( 

1152 "green" 

1153 if provider == "crowdcent" 

1154 else "yellow" 

1155 if provider == "numerai" 

1156 else "white" 

1157 ) 

1158 tree.add_row("[bold]DATA[/bold]", "") 

1159 if provider: 

1160 tree.add_row("├─ Provider", f"[{provider_color}]{provider}[/{provider_color}]") 

1161 tree.add_row("├─ Source", source_name) 

1162 tree.add_row("└─ Pred Col", str(config.pred_value_column)) 

1163 

1164 return Panel(tree, title="[bold cyan]BACKTEST CONFIG[/bold cyan]", box=box.HEAVY) 

1165 

1166 

1167def create_backtest_metrics_panel(stats: dict) -> Panel: 

1168 """Create metrics panel for backtest results.""" 

1169 # Create two-column layout for metrics 

1170 left_table = Table(show_header=False, box=None, padding=(0, 1)) 

1171 left_table.add_column("", width=15) 

1172 left_table.add_column("", justify="right") 

1173 

1174 right_table = Table(show_header=False, box=None, padding=(0, 1)) 

1175 right_table.add_column("", width=15) 

1176 right_table.add_column("", justify="right") 

1177 

1178 # Left column: Returns and basic metrics 

1179 total_return = stats.get("total_return", 0) 

1180 cagr = stats.get("cagr", 0) 

1181 final_equity = stats.get("final_equity", 0) 

1182 

1183 ret_color = "green" if total_return >= 0 else "red" 

1184 cagr_color = "green" if cagr >= 0 else "red" 

1185 

1186 left_table.add_row("DAYS", f"{stats.get('days', 0):,}") 

1187 left_table.add_row("FINAL EQUITY", format_currency(final_equity, compact=True)) 

1188 left_table.add_row("TOTAL RETURN", f"[{ret_color}]{total_return:.1%}[/{ret_color}]") 

1189 left_table.add_row("CAGR", f"[{cagr_color}]{cagr:.1%}[/{cagr_color}]") 

1190 left_table.add_row("WIN RATE", f"{stats.get('win_rate', 0):.1%}") 

1191 

1192 # Right column: Risk metrics 

1193 sharpe = stats.get("sharpe_ratio", 0) 

1194 sortino = stats.get("sortino_ratio", 0) 

1195 calmar = stats.get("calmar_ratio", 0) 

1196 max_dd = stats.get("max_drawdown", 0) 

1197 vol = stats.get("annual_volatility", 0) 

1198 

1199 # Color code risk metrics 

1200 sharpe_color = "green" if sharpe > 1 else "yellow" if sharpe > 0.5 else "red" 

1201 dd_color = "green" if max_dd > -0.1 else "yellow" if max_dd > -0.2 else "red" 

1202 

1203 right_table.add_row("SHARPE", f"[{sharpe_color}]{sharpe:.2f}[/{sharpe_color}]") 

1204 right_table.add_row("SORTINO", f"{sortino:.2f}") 

1205 right_table.add_row("CALMAR", f"{calmar:.2f}") 

1206 right_table.add_row("MAX DRAWDOWN", f"[{dd_color}]{max_dd:.1%}[/{dd_color}]") 

1207 right_table.add_row("VOLATILITY", f"{vol:.1%}") 

1208 

1209 return Panel( 

1210 Columns([left_table, right_table], expand=True), 

1211 title="[bold cyan]PERFORMANCE METRICS[/bold cyan]", 

1212 box=box.HEAVY, 

1213 ) 

1214 

1215 

1216def create_linechart_panel(daily_df, metric: str, color: str, y_label: str) -> Panel: 

1217 """Create line chart panel using plotext.""" 

1218 

1219 def plot_line(plt): 

1220 plt.plot(daily_df[metric], marker="braille", color=color) 

1221 plt.xlabel("Days") 

1222 plt.ylabel(y_label) 

1223 

1224 return create_plotext_panel( 

1225 plot_line, 

1226 f"[bold cyan]{metric.upper()}[/bold cyan]", 

1227 style=color, 

1228 height=9, 

1229 width=40, 

1230 ) 

1231 

1232 

1233def create_backtest_distributions(daily_df) -> Panel: 

1234 """Create backtest distributions using standardized small-multiples like optimize.""" 

1235 series_map: dict[str, dict[str, Any]] = {} 

1236 

1237 # Returns distribution 

1238 if "returns" in daily_df.columns: 

1239 returns = daily_df["returns"].to_list() 

1240 if returns: 

1241 series_map["RETURNS"] = {"values": returns, "color": "green", "bins": 15} 

1242 

1243 # Drawdown distribution 

1244 if "drawdown" in daily_df.columns: 

1245 drawdowns = daily_df["drawdown"].to_list() 

1246 if drawdowns: 

1247 series_map["DRAWDOWN"] = {"values": drawdowns, "color": "red", "bins": 15} 

1248 

1249 # Turnover distribution (non-zero only) 

1250 if "turnover" in daily_df.columns: 

1251 turnovers = daily_df["turnover"].to_list() 

1252 turnovers_nz = [t for t in turnovers if t and t != 0] 

1253 if turnovers_nz: 

1254 series_map["TURNOVER"] = { 

1255 "values": turnovers_nz, 

1256 "color": "yellow", 

1257 "bins": 15, 

1258 } 

1259 

1260 panel = ( 

1261 create_small_multiples_panel( 

1262 series_map, "[bold cyan]DAILY DISTRIBUTIONS[/bold cyan]" 

1263 ) 

1264 if series_map 

1265 else None 

1266 ) 

1267 if panel is not None: 

1268 return panel 

1269 return Panel("[dim]No distribution data available[/dim]", box=box.HEAVY) 

1270 

1271 

1272def create_latest_positions_panel(positions_df) -> Panel: 

1273 """Create panel showing latest position snapshot.""" 

1274 import polars as pl 

1275 

1276 if len(positions_df) == 0: 

1277 return Panel("[dim]No position data[/dim]", box=box.HEAVY) 

1278 

1279 # Get last rebalance date 

1280 latest_date = positions_df["date"].max() 

1281 latest_positions = positions_df.filter(positions_df["date"] == latest_date) 

1282 

1283 # Sort by absolute weight 

1284 latest_positions = latest_positions.with_columns( 

1285 pl.col("weight").abs().alias("abs_weight") 

1286 ).sort("abs_weight", descending=True) 

1287 

1288 table = Table( 

1289 show_header=True, 

1290 box=box.SIMPLE_HEAD, 

1291 header_style="bold cyan on #001926", 

1292 padding=(0, 1), 

1293 ) 

1294 

1295 table.add_column("COIN", style="cyan") 

1296 table.add_column("WEIGHT", justify="right") 

1297 

1298 # Count positions 

1299 long_count = sum( 

1300 1 for row in latest_positions.iter_rows(named=True) if row["weight"] > 0 

1301 ) 

1302 short_count = len(latest_positions) - long_count 

1303 

1304 for row in latest_positions.iter_rows(named=True): 

1305 weight = row["weight"] 

1306 weight_style = "green" if weight > 0 else "red" 

1307 

1308 table.add_row( 

1309 row["id"], 

1310 f"[{weight_style}]{abs(weight):.1%}[/{weight_style}]", 

1311 ) 

1312 

1313 title = ( 

1314 f"[bold cyan]FINAL POSITIONS[/bold cyan] [dim]│[/dim] " 

1315 f"[green]{long_count}L[/green] [red]{short_count}S[/red] [dim]│[/dim] " 

1316 f"{latest_date.strftime('%Y-%m-%d')}" 

1317 ) 

1318 

1319 return Panel(table, title=title, box=box.HEAVY) 

1320 

1321 

1322def create_enhanced_positions_table(positions_df, daily_df) -> Panel: 

1323 """Create enhanced positions table with detailed statistics.""" 

1324 import polars as pl 

1325 

1326 if len(positions_df) == 0: 

1327 return Panel("[dim]No position data[/dim]", box=box.HEAVY) 

1328 

1329 # Get unique dates for counting 

1330 unique_dates = positions_df["date"].unique().sort() 

1331 total_periods = len(unique_dates) 

1332 

1333 # Get last rebalance date 

1334 latest_date = positions_df["date"].max() 

1335 

1336 # Calculate statistics for each asset 

1337 position_stats = ( 

1338 positions_df.group_by("id") 

1339 .agg( 

1340 [ 

1341 # Count how many times this asset was held 

1342 pl.col("weight") 

1343 .filter(pl.col("weight") != 0) 

1344 .count() 

1345 .alias("times_held"), 

1346 # Average weight when held (non-zero) 

1347 pl.col("weight") 

1348 .filter(pl.col("weight") != 0) 

1349 .mean() 

1350 .alias("avg_weight"), 

1351 # Count longs vs shorts 

1352 pl.col("weight") 

1353 .filter(pl.col("weight") > 0) 

1354 .count() 

1355 .alias("times_long"), 

1356 pl.col("weight") 

1357 .filter(pl.col("weight") < 0) 

1358 .count() 

1359 .alias("times_short"), 

1360 # First and last appearance 

1361 pl.col("date").min().alias("first_date"), 

1362 pl.col("date").max().alias("last_date"), 

1363 # Current weight (from latest date) 

1364 pl.col("weight") 

1365 .filter(pl.col("date") == latest_date) 

1366 .first() 

1367 .alias("current_weight"), 

1368 ] 

1369 ) 

1370 .filter(pl.col("times_held") > 0) # Only include assets that were held 

1371 ) 

1372 

1373 # Add a column for predominant side 

1374 position_stats = position_stats.with_columns( 

1375 pl.when(pl.col("times_long") > pl.col("times_short")) 

1376 .then(pl.lit("LONG")) 

1377 .when(pl.col("times_short") > pl.col("times_long")) 

1378 .then(pl.lit("SHORT")) 

1379 .otherwise(pl.lit("MIXED")) 

1380 .alias("predominant_side") 

1381 ) 

1382 

1383 # Sort by times held (frequency) and then by absolute average weight 

1384 position_stats = position_stats.sort( 

1385 [ 

1386 pl.col("times_held").is_not_null(), # Non-null first 

1387 pl.col("times_held"), 

1388 pl.col("avg_weight").abs(), 

1389 ], 

1390 descending=[True, True, True], 

1391 ) 

1392 

1393 # Create the table 

1394 table = Table( 

1395 box=box.HEAVY_HEAD, 

1396 show_lines=False, 

1397 header_style="bold cyan on #001926", 

1398 expand=True, 

1399 ) 

1400 

1401 # Define columns 

1402 table.add_column("COIN", style="cyan", width=8) 

1403 table.add_column("SIDE", justify="center", width=8) 

1404 table.add_column("FREQUENCY", justify="right", width=10) 

1405 table.add_column("AVG WEIGHT", justify="right", width=10) 

1406 table.add_column("CURRENT", justify="right", width=10) 

1407 table.add_column("FIRST HELD", justify="center", width=12) 

1408 table.add_column("LAST HELD", justify="center", width=12) 

1409 table.add_column("CONSISTENCY", justify="center", width=12) 

1410 

1411 # Add rows 

1412 for row in position_stats.iter_rows(named=True): 

1413 coin = row["id"] 

1414 side = row["predominant_side"] 

1415 times_held = row["times_held"] or 0 

1416 avg_weight = row["avg_weight"] or 0 

1417 current_weight = row["current_weight"] or 0 

1418 first_date = row["first_date"] 

1419 last_date = row["last_date"] 

1420 

1421 # Determine side color 

1422 if side == "LONG": 

1423 side_style = "green" 

1424 elif side == "SHORT": 

1425 side_style = "red" 

1426 else: 

1427 side_style = "yellow" 

1428 

1429 # Format frequency as fraction and percentage 

1430 freq_pct = (times_held / total_periods * 100) if total_periods > 0 else 0 

1431 freq_str = f"{times_held}/{total_periods} ({freq_pct:.0f}%)" 

1432 

1433 # Format average weight 

1434 avg_weight_str = f"[{'green' if avg_weight > 0 else 'red'}]{abs(avg_weight):.1%}[/{'green' if avg_weight > 0 else 'red'}]" 

1435 

1436 # Format current weight 

1437 if current_weight == 0: 

1438 current_str = "[dim]-[/dim]" 

1439 else: 

1440 current_str = f"[{'green' if current_weight > 0 else 'red'}]{abs(current_weight):.1%}[/{'green' if current_weight > 0 else 'red'}]" 

1441 

1442 # Format dates 

1443 first_str = first_date.strftime("%Y-%m-%d") if first_date else "-" 

1444 last_str = last_date.strftime("%Y-%m-%d") if last_date else "-" 

1445 

1446 # Consistency indicator (visual bar) 

1447 consistency_bar = create_data_bar( 

1448 freq_pct, 100, width=8, filled_char="▓", empty_char="░" 

1449 ) 

1450 consistency_color = ( 

1451 "green" if freq_pct >= 75 else "yellow" if freq_pct >= 50 else "dim" 

1452 ) 

1453 consistency_str = ( 

1454 f"[{consistency_color}]{consistency_bar}[/{consistency_color}]" 

1455 ) 

1456 

1457 table.add_row( 

1458 f"[bold]{coin}[/bold]", 

1459 f"[{side_style}]{side}[/{side_style}]", 

1460 freq_str, 

1461 avg_weight_str, 

1462 current_str, 

1463 first_str, 

1464 last_str, 

1465 consistency_str, 

1466 ) 

1467 

1468 # Count current positions 

1469 current_positions = position_stats.filter(pl.col("current_weight") != 0) 

1470 current_longs = len(current_positions.filter(pl.col("current_weight") > 0)) 

1471 current_shorts = len(current_positions.filter(pl.col("current_weight") < 0)) 

1472 

1473 # Calculate some summary stats 

1474 total_unique_assets = len(position_stats) 

1475 most_consistent = position_stats.head(1) 

1476 if len(most_consistent) > 0: 

1477 top_asset = most_consistent["id"][0] 

1478 top_freq = most_consistent["times_held"][0] 

1479 top_pct = (top_freq / total_periods * 100) if total_periods > 0 else 0 

1480 consistency_note = f"Most consistent: {top_asset} ({top_pct:.0f}%)" 

1481 else: 

1482 consistency_note = "" 

1483 

1484 title = ( 

1485 f"[bold cyan]POSITION ANALYSIS[/bold cyan] [dim]│[/dim] " 

1486 f"{total_unique_assets} unique assets over {total_periods} periods [dim]│[/dim] " 

1487 f"Current: [green]{current_longs}L[/green] [red]{current_shorts}S[/red]" 

1488 ) 

1489 

1490 if consistency_note: 

1491 title += f" [dim]│[/dim] {consistency_note}" 

1492 

1493 return Panel(table, title=title, box=box.HEAVY) 

1494 

1495 

1496def display_optimization_results( 

1497 console: Console, results_df, metric: str, top_n: int = 20, config=None 

1498): 

1499 """Display optimization results.""" 

1500 

1501 # Disclaimer warning first 

1502 disclaimer = Panel( 

1503 Text.from_markup( 

1504 "[yellow]\n Optimized parameters are based on historical data and may be overfit. " 

1505 "Past optimal parameters may not remain optimal in future market conditions.[/yellow]\n", 

1506 justify="center", 

1507 ), 

1508 title="[bold yellow] OPTIMIZATION DISCLAIMER [/bold yellow]", 

1509 box=box.HEAVY, 

1510 border_style="yellow", 

1511 ) 

1512 console.print(disclaimer) 

1513 

1514 layout = Layout() 

1515 layout.split_column(Layout(name="header", size=3), Layout(name="body")) 

1516 

1517 # Header 

1518 header = Panel( 

1519 Text( 

1520 f"OPTIMIZATION RESULTS :: {metric.upper()}", 

1521 style="bold cyan", 

1522 justify="center", 

1523 ), 

1524 box=DOUBLE, 

1525 style="cyan", 

1526 ) 

1527 layout["header"].update(header) 

1528 

1529 # Body: Main results + best params summary 

1530 layout["body"].split_row( 

1531 Layout(name="results", ratio=3), Layout(name="summary", ratio=1) 

1532 ) 

1533 

1534 # Results table 

1535 results_panel = create_optimization_results_table(results_df, metric, top_n) 

1536 layout["results"].update(results_panel) 

1537 

1538 # Summary panel with best parameters 

1539 if len(results_df) > 0: 

1540 summary_panel = create_optimization_summary_panel( 

1541 results_df.head(1), metric, config 

1542 ) 

1543 layout["summary"].update(summary_panel) 

1544 else: 

1545 layout["summary"].update(Panel("[dim]No results[/dim]", box=box.HEAVY)) 

1546 

1547 console.print(layout) 

1548 

1549 sm_panel = create_optimization_small_multiples_panel(results_df) 

1550 if sm_panel is not None: 

1551 console.print(sm_panel) 

1552 

1553 

1554def _create_hist_panel( 

1555 values, title: str, color: str, bins: int = 15, width: int = 24, height: int = 6 

1556) -> Panel: 

1557 """Create a compact histogram panel for small multiples.""" 

1558 

1559 def plot_hist(plt): 

1560 if not values: 

1561 return 

1562 plt.hist(values, bins=bins, color=color) 

1563 plt.xlabel("") 

1564 plt.ylabel("") 

1565 

1566 return create_plotext_panel( 

1567 plot_hist, f"[bold]{title}[/bold]", width=width, height=height, style=color 

1568 ) 

1569 

1570 

1571def create_small_multiples_panel( 

1572 series_map: dict[str, dict[str, Any]], title: str 

1573) -> Panel | None: 

1574 """Create a small-multiples panel from a mapping of label -> values.""" 

1575 panels = [] 

1576 # Fixed sizes to encourage consistent visual scale 

1577 for label, conf in series_map.items(): 

1578 values = conf.get("values", []) 

1579 if values is None: 

1580 values = [] 

1581 color = conf.get("color", "cyan") 

1582 bins = conf.get("bins", 15) 

1583 p = _create_hist_panel(values, label, color=color, bins=bins) 

1584 panels.append(p) 

1585 if not panels: 

1586 return None 

1587 return Panel(Columns(panels, expand=True), title=title, box=box.HEAVY) 

1588 

1589 

1590def create_optimization_small_multiples_panel(results_df) -> Panel | None: 

1591 """Build small-multiples histograms for optimization metrics: Sharpe, CAGR, Calmar, Max DD, Volatility.""" 

1592 try: 

1593 cols = set(results_df.columns) 

1594 series_map = {} 

1595 if "sharpe" in cols: 

1596 series_map["SHARPE"] = { 

1597 "values": results_df["sharpe"].to_list(), 

1598 "color": "cyan", 

1599 } 

1600 if "cagr" in cols: 

1601 series_map["CAGR"] = { 

1602 "values": results_df["cagr"].to_list(), 

1603 "color": "green", 

1604 } 

1605 if "calmar" in cols: 

1606 series_map["CALMAR"] = { 

1607 "values": results_df["calmar"].to_list(), 

1608 "color": "blue", 

1609 } 

1610 if "max_dd" in cols: 

1611 series_map["MAX DD"] = { 

1612 "values": results_df["max_dd"].to_list(), 

1613 "color": "red", 

1614 } 

1615 if "volatility" in cols: 

1616 series_map["VOL"] = { 

1617 "values": results_df["volatility"].to_list(), 

1618 "color": "yellow", 

1619 } 

1620 if not series_map: 

1621 return None 

1622 return create_small_multiples_panel( 

1623 series_map, "[bold cyan]METRIC DISTRIBUTIONS[/bold cyan]" 

1624 ) 

1625 except Exception: 

1626 return None 

1627 

1628 

1629def create_optimization_results_table(results_df, metric: str, top_n: int) -> Panel: 

1630 """Create optimization results table panel.""" 

1631 table = Table( 

1632 box=box.HEAVY_HEAD, 

1633 show_lines=False, 

1634 header_style="bold cyan on #001926", 

1635 expand=True, 

1636 ) 

1637 

1638 # Add columns with consistent styling 

1639 table.add_column("RANK", style="dim", width=4, justify="right") 

1640 table.add_column("LONG", style="green", justify="right", width=6) 

1641 table.add_column("SHORT", style="red", justify="right", width=6) 

1642 table.add_column("LEV", style="yellow", justify="right", width=6) 

1643 table.add_column("DAYS", style="cyan", justify="right", width=6) 

1644 table.add_column("POWER", style="magenta", justify="right", width=6) 

1645 table.add_column( 

1646 "SHARPE", 

1647 justify="right", 

1648 width=8, 

1649 style="bold white" if metric == "sharpe" else "dim", 

1650 ) 

1651 table.add_column( 

1652 "CAGR", 

1653 justify="right", 

1654 width=8, 

1655 style="bold white" if metric == "cagr" else "dim", 

1656 ) 

1657 table.add_column( 

1658 "CALMAR", 

1659 justify="right", 

1660 width=8, 

1661 style="bold white" if metric == "calmar" else "dim", 

1662 ) 

1663 table.add_column("MAX DD", justify="right", width=8) 

1664 table.add_column("EQUITY", justify="right", width=10) 

1665 

1666 for i, row in enumerate(results_df.head(top_n).iter_rows(named=True), 1): 

1667 # Color code metrics based on performance 

1668 sharpe = row["sharpe"] 

1669 sharpe_color = "green" if sharpe > 1 else "yellow" if sharpe > 0.5 else "red" 

1670 

1671 dd = row["max_dd"] 

1672 dd_color = "green" if dd > -0.1 else "yellow" if dd > -0.2 else "red" 

1673 

1674 cagr = row["cagr"] 

1675 cagr_color = "green" if cagr > 0.2 else "yellow" if cagr > 0 else "red" 

1676 

1677 # Special highlighting for top 3 

1678 rank_style = "bold cyan" if i == 1 else "cyan" if i <= 3 else "dim" 

1679 

1680 table.add_row( 

1681 f"[{rank_style}]{i}[/{rank_style}]", 

1682 str(row["num_long"]), 

1683 str(row["num_short"]), 

1684 f"{row['leverage']:.1f}x", 

1685 str(row["rebalance_days"]), 

1686 f"{row['rank_power']:.1f}", 

1687 f"[{sharpe_color}]{sharpe:.2f}[/{sharpe_color}]", 

1688 f"[{cagr_color}]{cagr:.1%}[/{cagr_color}]", 

1689 f"{row['calmar']:.2f}", 

1690 f"[{dd_color}]{dd:.1%}[/{dd_color}]", 

1691 format_currency(row["final_equity"], compact=True), 

1692 ) 

1693 

1694 # Calculate title with statistics 

1695 total_tested = len(results_df) 

1696 positive_sharpe = sum( 

1697 1 for row in results_df.iter_rows(named=True) if row["sharpe"] > 0 

1698 ) 

1699 

1700 title = ( 

1701 f"[bold cyan]TOP {top_n} COMBINATIONS[/bold cyan] [dim]│[/dim] " 

1702 f"Tested {total_tested} [dim]│[/dim] " 

1703 f"Positive Sharpe {positive_sharpe}/{total_tested} ({positive_sharpe / total_tested * 100:.1f}%)" 

1704 ) 

1705 

1706 return Panel(table, title=title, box=box.HEAVY) 

1707 

1708 

1709def create_optimization_summary_panel(best_row_df, metric: str, config=None) -> Panel: 

1710 """Create summary panel for best parameters.""" 

1711 best = best_row_df.iter_rows(named=True).__next__() 

1712 

1713 # Create metrics table 

1714 table = Table(show_header=False, box=None, padding=(0, 1)) 

1715 table.add_column("", width=12) 

1716 table.add_column("", justify="right") 

1717 

1718 # Best configuration 

1719 table.add_row("[bold]BEST CONFIG[/bold]", "") 

1720 table.add_row("", "") 

1721 table.add_row("LONG", f"[green]{best['num_long']}[/green]") 

1722 table.add_row("SHORT", f"[red]{best['num_short']}[/red]") 

1723 table.add_row("LEVERAGE", f"[yellow]{best['leverage']:.1f}x[/yellow]") 

1724 table.add_row("REBALANCE", f"{best['rebalance_days']} days") 

1725 table.add_row("RANK POWER", f"[magenta]{best['rank_power']:.1f}[/magenta]") 

1726 table.add_row("", "") 

1727 

1728 # Performance metrics 

1729 table.add_row("[bold]METRICS[/bold]", "") 

1730 table.add_row("", "") 

1731 

1732 # Highlight the optimization metric 

1733 if metric == "sharpe": 

1734 table.add_row("SHARPE", f"[bold cyan]{best['sharpe']:.2f}[/bold cyan]") 

1735 table.add_row("CAGR", f"{best['cagr']:.1%}") 

1736 table.add_row("CALMAR", f"{best['calmar']:.2f}") 

1737 elif metric == "cagr": 

1738 table.add_row("SHARPE", f"{best['sharpe']:.2f}") 

1739 table.add_row("CAGR", f"[bold cyan]{best['cagr']:.1%}[/bold cyan]") 

1740 table.add_row("CALMAR", f"{best['calmar']:.2f}") 

1741 else: # calmar 

1742 table.add_row("SHARPE", f"{best['sharpe']:.2f}") 

1743 table.add_row("CAGR", f"{best['cagr']:.1%}") 

1744 table.add_row("CALMAR", f"[bold cyan]{best['calmar']:.2f}[/bold cyan]") 

1745 

1746 table.add_row("MAX DD", f"{best['max_dd']:.1%}") 

1747 table.add_row("VOLATILITY", f"{best['volatility']:.1%}") 

1748 table.add_row("", "") 

1749 table.add_row("FINAL EQUITY", format_currency(best["final_equity"])) 

1750 

1751 # Add data source information if config is provided 

1752 if config: 

1753 table.add_row("", "") 

1754 table.add_row("[bold]DATA[/bold]", "") 

1755 table.add_row("", "") 

1756 

1757 # Provider 

1758 provider = getattr(config, "data_provider", None) 

1759 if provider: 

1760 provider_color = ( 

1761 "green" 

1762 if provider == "crowdcent" 

1763 else "yellow" 

1764 if provider == "numerai" 

1765 else "white" 

1766 ) 

1767 table.add_row( 

1768 "PROVIDER", f"[{provider_color}]{provider}[/{provider_color}]" 

1769 ) 

1770 

1771 # Source file 

1772 source_name = ( 

1773 config.predictions_path.split("/")[-1] 

1774 if "/" in config.predictions_path 

1775 else config.predictions_path 

1776 ) 

1777 table.add_row( 

1778 "SOURCE", source_name[:12] + "..." if len(source_name) > 15 else source_name 

1779 ) 

1780 

1781 # Prediction column 

1782 table.add_row("PRED COL", str(config.pred_value_column)[:15]) 

1783 

1784 return Panel( 

1785 table, title=f"[bold cyan]BEST BY {metric.upper()}[/bold cyan]", box=box.HEAVY 

1786 ) 

1787 

1788 

1789def create_optimization_progress_display( 

1790 current: int, 

1791 total: int, 

1792 current_params: dict, 

1793 best_so_far: dict | None = None, 

1794 elapsed_time: float = 0, 

1795) -> Layout: 

1796 """Create live progress display for optimization.""" 

1797 from rich.layout import Layout 

1798 from rich.progress import Progress, BarColumn, TextColumn, TimeRemainingColumn 

1799 from rich.align import Align 

1800 

1801 layout = Layout() 

1802 layout.split_column( 

1803 Layout(name="header", size=3), 

1804 Layout(name="body", size=12), 

1805 Layout(name="footer", size=3), 

1806 ) 

1807 

1808 # Header 

1809 header = Panel( 

1810 Text("OPTIMIZATION IN PROGRESS", style="bold cyan blink", justify="center"), 

1811 box=DOUBLE, 

1812 style="cyan", 

1813 ) 

1814 layout["header"].update(header) 

1815 

1816 # Body: Progress bar and current/best params 

1817 layout["body"].split_column( 

1818 Layout(name="progress", size=5), Layout(name="params", size=7) 

1819 ) 

1820 

1821 # Create progress bar 

1822 progress = Progress( 

1823 TextColumn("[bold cyan]Testing:[/bold cyan]"), 

1824 BarColumn(bar_width=40, style="cyan", complete_style="green"), 

1825 TextColumn("{task.completed}/{task.total}"), 

1826 TextColumn("[cyan]{task.percentage:>3.0f}%[/cyan]"), 

1827 TimeRemainingColumn(), 

1828 expand=False, 

1829 ) 

1830 

1831 task = progress.add_task("Optimization", total=total, completed=current) 

1832 progress.update(task, completed=current) 

1833 

1834 progress_panel = Panel( 

1835 Align.center(progress, vertical="middle"), 

1836 box=box.HEAVY, 

1837 title="[bold cyan]PROGRESS[/bold cyan]", 

1838 ) 

1839 layout["progress"].update(progress_panel) 

1840 

1841 # Parameters display 

1842 layout["params"].split_row( 

1843 Layout(name="current", ratio=1), Layout(name="best", ratio=1) 

1844 ) 

1845 

1846 # Current parameters 

1847 current_table = Table(show_header=False, box=None, padding=(0, 1)) 

1848 current_table.add_column("", width=12) 

1849 current_table.add_column("", justify="right") 

1850 

1851 current_table.add_row("COMBINATION", f"{current}/{total}") 

1852 current_table.add_row("", "") 

1853 current_table.add_row( 

1854 "LONG", f"[green]{current_params.get('num_long', '-')}[/green]" 

1855 ) 

1856 current_table.add_row("SHORT", f"[red]{current_params.get('num_short', '-')}[/red]") 

1857 current_table.add_row( 

1858 "LEVERAGE", f"[yellow]{current_params.get('leverage', 0):.1f}x[/yellow]" 

1859 ) 

1860 current_table.add_row("DAYS", f"{current_params.get('rebalance_days', '-')}") 

1861 

1862 layout["current"].update( 

1863 Panel(current_table, title="[bold cyan]TESTING[/bold cyan]", box=box.HEAVY) 

1864 ) 

1865 

1866 # Best so far 

1867 if best_so_far: 

1868 best_table = Table(show_header=False, box=None, padding=(0, 1)) 

1869 best_table.add_column("", width=12) 

1870 best_table.add_column("", justify="right") 

1871 

1872 best_table.add_row( 

1873 "SHARPE", f"[green]{best_so_far.get('sharpe', 0):.2f}[/green]" 

1874 ) 

1875 best_table.add_row("CAGR", f"{best_so_far.get('cagr', 0):.1%}") 

1876 best_table.add_row("", "") 

1877 best_table.add_row("CONFIG", "") 

1878 best_table.add_row( 

1879 "L/S", 

1880 f"{best_so_far.get('num_long', '-')}/{best_so_far.get('num_short', '-')}", 

1881 ) 

1882 best_table.add_row("LEV", f"{best_so_far.get('leverage', 0):.1f}x") 

1883 

1884 layout["best"].update( 

1885 Panel( 

1886 best_table, 

1887 title="[bold green]BEST SO FAR[/bold green]", 

1888 box=box.HEAVY, 

1889 border_style="green", 

1890 ) 

1891 ) 

1892 else: 

1893 layout["best"].update( 

1894 Panel( 

1895 "[dim]No results yet[/dim]", 

1896 title="[bold]BEST SO FAR[/bold]", 

1897 box=box.HEAVY, 

1898 ) 

1899 ) 

1900 

1901 # Footer 

1902 eta = (elapsed_time / current * (total - current)) if current > 0 else 0 

1903 eta_str = f"{int(eta // 60)}:{int(eta % 60):02d}" if eta > 0 else "--:--" 

1904 

1905 footer_text = ( 

1906 f"[dim]Elapsed: {int(elapsed_time // 60)}:{int(elapsed_time % 60):02d}" 

1907 f"ETA: {eta_str}" 

1908 f"Rate: {current / elapsed_time:.1f}/s[/dim]" 

1909 if elapsed_time > 0 

1910 else "[dim]Starting...[/dim]" 

1911 ) 

1912 

1913 layout["footer"].update(Panel(footer_text, box=box.HEAVY, style="dim")) 

1914 

1915 return layout 

1916 

1917 

1918def display_optimization_contours(console: Console, results_df, metric: str): 

1919 """Display parameter heatmaps using ASCII visualization as small multiples.""" 

1920 import polars as pl 

1921 

1922 leverages = results_df["leverage"].unique().sort().to_list() 

1923 if not leverages: 

1924 console.print(Panel("[dim]No data to visualize[/dim]", box=box.HEAVY)) 

1925 return 

1926 

1927 header = Panel( 

1928 Text( 

1929 f"PARAMETER HEATMAPS :: {metric.upper()}", 

1930 style="bold cyan", 

1931 justify="center", 

1932 ), 

1933 box=DOUBLE, 

1934 style="cyan", 

1935 ) 

1936 console.print(header) 

1937 

1938 # Collect heatmap panels for a grid layout (up to 2x3) 

1939 heatmap_panels = [] 

1940 for leverage in leverages: 

1941 df = results_df.filter(pl.col("leverage") == leverage) 

1942 

1943 if len(df) < 4: 

1944 heatmap_panels.append( 

1945 Panel( 

1946 "[dim]Insufficient data[/dim]", 

1947 title=f"[bold]LEV {leverage:.1f}x[/bold]", 

1948 box=box.HEAVY, 

1949 height=12, 

1950 ) 

1951 ) 

1952 continue 

1953 

1954 pivot = df.pivot( 

1955 index="num_long", on="num_short", values=metric, aggregate_function="mean" 

1956 ) 

1957 

1958 longs = sorted(pivot["num_long"].to_list()) 

1959 shorts = sorted([c for c in pivot.columns if c != "num_long"]) 

1960 

1961 heatmap_str = create_ascii_heatmap(pivot, longs, shorts, metric) 

1962 best = df[metric].max() 

1963 worst = df[metric].min() 

1964 subtitle = ( 

1965 f"[bold cyan]LEV {leverage:.1f}x[/bold cyan] [dim]│[/dim] " 

1966 f"Best {best:.3f} [dim]│[/dim] Worst {worst:.3f}" 

1967 ) 

1968 

1969 heatmap_panels.append( 

1970 Panel( 

1971 heatmap_str, 

1972 title=subtitle, 

1973 box=box.HEAVY, 

1974 height=12, 

1975 ) 

1976 ) 

1977 

1978 # Arrange panels into small multiples grid and print once 

1979 if heatmap_panels: 

1980 num_panels = len(heatmap_panels) 

1981 # Choose columns per row for pleasant layout 

1982 if num_panels == 1: 

1983 cols_per_row = 1 

1984 elif num_panels in (2, 4): 

1985 cols_per_row = 2 

1986 else: 

1987 cols_per_row = 3 

1988 

1989 rows = [] 

1990 for i in range(0, num_panels, cols_per_row): 

1991 rows.append(Columns(heatmap_panels[i : i + cols_per_row], expand=True)) 

1992 

1993 from rich.table import Table as RichTable 

1994 

1995 container = RichTable.grid(expand=True) 

1996 for row in rows: 

1997 container.add_row(row) 

1998 

1999 console.print(container) 

2000 

2001 

2002def create_ascii_heatmap(pivot_df, longs: list, shorts: list, metric: str) -> str: 

2003 """Create an ASCII heatmap representation with sorted axes, legend-aware colors, and best-cell emphasis.""" 

2004 import polars as pl 

2005 

2006 # Ensure numeric sort order (5, 10, 15 ...) 

2007 longs = sorted(longs) 

2008 shorts = sorted(shorts) 

2009 

2010 # Find best cell for emphasis 

2011 best_val = None 

2012 best_coord = None 

2013 for long_val in longs: 

2014 row = pivot_df.filter(pl.col("num_long") == long_val) 

2015 for short_val in shorts: 

2016 if len(row) > 0 and str(short_val) in row.columns: 

2017 val = row[str(short_val)][0] 

2018 if val is not None and (best_val is None or val > best_val): 

2019 best_val = val 

2020 best_coord = (long_val, short_val) 

2021 

2022 # Build the heatmap as a string 

2023 lines = [] 

2024 header = " L\\S │ " + " ".join(f"{s:>4}" for s in shorts) 

2025 lines.append(header) 

2026 lines.append("─" * len(header)) 

2027 

2028 for long_val in longs: 

2029 row_data = pivot_df.filter(pl.col("num_long") == long_val) 

2030 row_str = f" {long_val:>6}" 

2031 if len(row_data) > 0: 

2032 for short_val in shorts: 

2033 if str(short_val) in row_data.columns: 

2034 val = row_data[str(short_val)][0] 

2035 if val is not None: 

2036 # Color buckets for sharpe-like metrics 

2037 if metric == "sharpe": 

2038 if val > 1.5: 

2039 txt = f"[green]{val:>4.1f}[/green]" 

2040 elif val > 0: 

2041 txt = f"[yellow]{val:>4.1f}[/yellow]" 

2042 else: 

2043 txt = f"[red]{val:>4.1f}[/red]" 

2044 else: 

2045 txt = f"{val:>4.1f}" 

2046 # Emphasize best cell 

2047 if best_coord == (long_val, short_val): 

2048 txt = f"[bold]{txt}[/bold]" 

2049 row_str += txt + " " 

2050 else: 

2051 row_str += " - " 

2052 else: 

2053 row_str += " - " 

2054 else: 

2055 row_str += " - " * len(shorts) 

2056 lines.append(row_str) 

2057 return "\n".join(lines) 

2058 

2059 

2060def create_open_orders_panel(orders: list["Order"]) -> Panel: 

2061 """Create panel showing active orders.""" 

2062 from rich.table import Table 

2063 from rich.panel import Panel 

2064 from rich import box 

2065 

2066 table = Table( 

2067 box=box.HEAVY_HEAD, 

2068 show_lines=False, 

2069 header_style="bold cyan on #001926", 

2070 expand=True, 

2071 ) 

2072 

2073 table.add_column("COIN", style="cyan", width=8) 

2074 table.add_column("SIDE", justify="center", width=6) 

2075 table.add_column("STRATEGY", justify="center", width=12) 

2076 table.add_column("PROGRESS", justify="right", width=10) 

2077 table.add_column("FILLED", justify="right", width=12) 

2078 table.add_column("SLIPPAGE", justify="right", width=10) 

2079 table.add_column("STATUS", justify="center", width=12) 

2080 table.add_column("AGE", justify="right", width=8) 

2081 

2082 now = datetime.now(timezone.utc) 

2083 

2084 for order in sorted(orders, key=lambda o: o.timestamp, reverse=True): 

2085 # Calculate progress 

2086 progress = (order.filled_size / order.size * 100) if order.size > 0 else 0 

2087 

2088 # Format filled size 

2089 filled_str = f"{order.filled_size:.2f}/{order.size:.2f}" 

2090 

2091 # Slippage color 

2092 if order.slippage_bps is not None: 

2093 slippage_color = "green" if order.slippage_bps <= 5 else "yellow" if order.slippage_bps <= 15 else "red" 

2094 slippage_str = f"[{slippage_color}]{order.slippage_bps:+.1f}bps[/{slippage_color}]" 

2095 else: 

2096 slippage_str = "[dim]-[/dim]" 

2097 

2098 # Status color 

2099 status_colors = { 

2100 "pending": "yellow", 

2101 "open": "cyan", 

2102 "partially_filled": "blue", 

2103 "filled": "green", 

2104 "cancelled": "dim", 

2105 "failed": "red" 

2106 } 

2107 status_color = status_colors.get(order.status, "white") 

2108 status_str = f"[{status_color}]{order.status.upper().replace('_', ' ')}[/{status_color}]" 

2109 

2110 # Age 

2111 age = now - order.timestamp 

2112 if age.total_seconds() < 3600: 

2113 age_str = f"{int(age.total_seconds() / 60)}m" 

2114 elif age.total_seconds() < 86400: 

2115 age_str = f"{age.total_seconds() / 3600:.1f}h" 

2116 else: 

2117 age_str = f"{age.days}d" 

2118 

2119 table.add_row( 

2120 f"[bold]{order.coin}[/bold]", 

2121 f"[{'green' if order.side == 'BUY' else 'red'}]{order.side}[/{'green' if order.side == 'BUY' else 'red'}]", 

2122 order.strategy.replace("_", " ").upper(), 

2123 f"{progress:.0f}%", 

2124 filled_str, 

2125 slippage_str, 

2126 status_str, 

2127 age_str 

2128 ) 

2129 

2130 title = f"[bold cyan]OPEN ORDERS[/bold cyan] [dim]│[/dim] {len(orders)} active" 

2131 return Panel(table, title=title, box=box.HEAVY) 

2132 

2133 

2134def create_order_history_panel(history: "OrderHistory", days: int) -> Panel: 

2135 """Create panel showing order history and stats.""" 

2136 from rich.table import Table 

2137 from rich.panel import Panel 

2138 from rich.columns import Columns 

2139 from rich import box 

2140 

2141 # Stats table 

2142 stats_table = Table(show_header=False, box=None, padding=(0, 1)) 

2143 stats_table.add_column("", width=18) 

2144 stats_table.add_column("", justify="right") 

2145 

2146 stats_table.add_row("[bold]SUMMARY[/bold]", "") 

2147 stats_table.add_row("PERIOD", f"{days} days") 

2148 stats_table.add_row("TOTAL ORDERS", str(len(history.orders))) 

2149 stats_table.add_row("FILL RATE", f"{history.fill_rate:.1%}") 

2150 stats_table.add_row("", "") 

2151 stats_table.add_row("[bold]PERFORMANCE[/bold]", "") 

2152 

2153 slippage_color = "green" if history.total_slippage_bps <= 10 else "yellow" if history.total_slippage_bps <= 25 else "red" 

2154 stats_table.add_row( 

2155 "AVG SLIPPAGE", 

2156 f"[{slippage_color}]{history.total_slippage_bps:.1f}bps[/{slippage_color}]" 

2157 ) 

2158 stats_table.add_row("TOTAL FEES", format_currency(history.total_fees)) 

2159 

2160 if history.avg_fill_time_minutes > 0: 

2161 stats_table.add_row("AVG FILL TIME", f"{history.avg_fill_time_minutes:.0f}min") 

2162 

2163 # Recent orders table 

2164 orders_table = Table( 

2165 box=box.SIMPLE_HEAD, 

2166 show_lines=False, 

2167 header_style="bold cyan", 

2168 ) 

2169 

2170 orders_table.add_column("DATE", width=12) 

2171 orders_table.add_column("COIN", width=6) 

2172 orders_table.add_column("SIDE", width=4) 

2173 orders_table.add_column("SIZE", justify="right", width=10) 

2174 orders_table.add_column("SLIP", justify="right", width=8) 

2175 orders_table.add_column("STATUS", width=10) 

2176 

2177 for order in history.orders[:20]: # Show last 20 

2178 date_str = order.timestamp.strftime("%Y-%m-%d") 

2179 side_color = "green" if order.side == "BUY" else "red" 

2180 

2181 if order.slippage_bps is not None: 

2182 slippage_color = "green" if abs(order.slippage_bps) <= 10 else "yellow" 

2183 slip_str = f"[{slippage_color}]{order.slippage_bps:+.1f}[/{slippage_color}]" 

2184 else: 

2185 slip_str = "-" 

2186 

2187 status_color = "green" if order.status == "filled" else "red" if order.status == "failed" else "dim" 

2188 

2189 orders_table.add_row( 

2190 date_str, 

2191 order.coin, 

2192 f"[{side_color}]{order.side[0]}[/{side_color}]", 

2193 f"{order.filled_size:.2f}", 

2194 slip_str, 

2195 f"[{status_color}]{order.status[:8]}[/{status_color}]" 

2196 ) 

2197 

2198 layout = Columns([ 

2199 Panel(stats_table, title="[bold cyan]STATS[/bold cyan]", box=box.HEAVY), 

2200 Panel(orders_table, title="[bold cyan]RECENT ORDERS[/bold cyan]", box=box.HEAVY) 

2201 ], expand=True) 

2202 

2203 return Panel(layout, title=f"[bold cyan]ORDER HISTORY :: {days} DAYS[/bold cyan]", box=box.HEAVY)