Coverage for src / monte_neo / utils / visualization.py: 84%

79 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-28 16:27 +0200

1"""Visualization utilities for backtest results. 

2""" 

3 

4from typing import Any 

5 

6import matplotlib.pyplot as plt 

7import numpy as np 

8import pandas as pd 

9import seaborn as sns 

10 

11 

12def plot_equity_curves(results: list[dict[str, Any]], title: str = "Monte Carlo Equity Curves"): 

13 """Plot equity curves for all scenarios.""" 

14 plt.figure(figsize=(12, 6)) 

15 

16 # If results contain equity history, plot them. 

17 # Currently our results only contain final metrics. 

18 # To plot equity curves, we need to return them from the engine. 

19 # For now, let's plot a distribution of final returns. 

20 

21 returns = [res['metrics']['total_return'] for res in results] 

22 

23 sns.histplot(returns, kde=True) 

24 plt.axvline(np.mean(returns), color='r', linestyle='--', label=f'Mean: {np.mean(returns):.2%}') 

25 plt.axvline(0, color='k', linestyle='-') 

26 plt.title(title) 

27 plt.xlabel("Total Return") 

28 plt.ylabel("Frequency") 

29 plt.legend() 

30 plt.grid(True, alpha=0.3) 

31 return plt 

32 

33def plot_drawdown_dist(results: list[dict[str, Any]], title: str = "Max Drawdown Distribution"): 

34 """Plot distribution of maximum drawdowns.""" 

35 plt.figure(figsize=(10, 5)) 

36 drawdowns = [res['metrics']['max_drawdown'] for res in results] 

37 

38 sns.histplot(drawdowns, kde=True, color='orange') 

39 plt.axvline(np.mean(drawdowns), color='r', linestyle='--', label=f'Mean DD: {np.mean(drawdowns):.2%}') 

40 plt.title(title) 

41 plt.xlabel("Max Drawdown") 

42 plt.ylabel("Frequency") 

43 plt.legend() 

44 plt.grid(True, alpha=0.3) 

45 return plt 

46 

47def plot_metrics_summary(results: list[dict[str, Any]]): 

48 """Plot a summary table/heatmap of key metrics.""" 

49 metrics_df = pd.DataFrame([res['metrics'] for res in results]) 

50 

51 fig, axes = plt.subplots(2, 2, figsize=(15, 10)) 

52 

53 sns.boxplot(y=metrics_df['total_return'], ax=axes[0,0], color='skyblue') 

54 axes[0,0].set_title("Total Return") 

55 

56 sns.boxplot(y=metrics_df['sharpe_ratio'], ax=axes[0,1], color='lightgreen') 

57 axes[0,1].set_title("Sharpe Ratio") 

58 

59 sns.boxplot(y=metrics_df['win_rate'], ax=axes[1,0], color='salmon') 

60 axes[1,0].set_title("Win Rate") 

61 

62 sns.boxplot(y=metrics_df['max_drawdown'], ax=axes[1,1], color='orange') 

63 axes[1,1].set_title("Max Drawdown") 

64 

65 plt.tight_layout() 

66 return fig 

67 

68def plot_stress_test_summary(stress_results: dict[str, Any]): 

69 """Plot a summary of stress test results.""" 

70 plt.figure(figsize=(10, 6)) 

71 

72 names = [] 

73 values = [] 

74 

75 # 1. Black Swan Impact 

76 bs = stress_results.get("black_swan", {}) 

77 if bs: 

78 names.append("Black Swan Ret") 

79 values.append(bs.get("total_return", 0)) 

80 

81 # 2. Sensitivity 

82 sens = stress_results.get("sensitivity", {}) 

83 if sens: 

84 names.append("Sens. Std Var") 

85 values.append(sens.get("std_return_variation", 0)) 

86 

87 # 3. Breaking Point 

88 bp = stress_results.get("breaking_point", {}) 

89 if bp: 

90 bp_val = bp.get("breaking_point_bps", 0) 

91 if isinstance(bp_val, str) and ">" in bp_val: 

92 bp_val = float(bp_val.replace(">", "").strip()) 

93 names.append("Break Pt (bps)/100") # Scaled for plotting 

94 values.append(float(bp_val) / 100.0) 

95 

96 plt.bar(names, values, color=['red', 'blue', 'green']) 

97 plt.axhline(0, color='black', linewidth=1) 

98 plt.title("Deep Stress Test Summary") 

99 plt.ylabel("Metric Value") 

100 plt.grid(True, alpha=0.3) 

101 

102 return plt 

103 

104def plot_sensitivity_heatmap(sensitivity_results: dict[str, Any]): 

105 """Plot a heatmap of parameter sensitivity.""" 

106 grid = sensitivity_results.get("grid") 

107 if not grid or not grid.get("matrix"): 

108 return None 

109 

110 plt.figure(figsize=(10, 8)) 

111 

112 matrix = np.array(grid["matrix"]) 

113 p1_vals = [f"{v:.2f}" for v in grid["p1_values"]] 

114 p2_vals = [f"{v:.2f}" for v in grid["p2_values"]] 

115 

116 sns.heatmap(matrix, annot=True, fmt=".2%", xticklabels=p2_vals, yticklabels=p1_vals, cmap="RdYlGn") 

117 plt.title(f"Sensitivity: {grid['p1_name']} vs {grid['p2_name']}") 

118 plt.xlabel(grid["p2_name"]) 

119 plt.ylabel(grid["p1_name"]) 

120 

121 return plt