Skip to content

Commit dd7c9c7

Browse files
Hi
1 parent 9797c8f commit dd7c9c7

File tree

20 files changed

+467
-327
lines changed

20 files changed

+467
-327
lines changed

meridianalgo/analytics/tear_sheets.py

Lines changed: 121 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -482,18 +482,41 @@ def plot_position_exposure(self, ax: Optional[Any] = None) -> Any:
482482
else:
483483
gross_exposure = self.gross_lev
484484
# Estimate net from positions if available, else assume 0
485-
net_exposure = self.positions.sum(axis=1) if not self.positions.empty else pd.Series(0, index=self.returns.index)
485+
net_exposure = (
486+
self.positions.sum(axis=1)
487+
if not self.positions.empty
488+
else pd.Series(0, index=self.returns.index)
489+
)
490+
491+
ax.plot(
492+
gross_exposure.index,
493+
gross_exposure.values,
494+
label="Gross Exposure",
495+
color="black",
496+
linewidth=1,
497+
)
498+
ax.plot(
499+
net_exposure.index,
500+
net_exposure.values,
501+
label="Net Exposure",
502+
color=self.COLORS["strategy"],
503+
linewidth=1,
504+
linestyle="--",
505+
)
486506

487-
ax.plot(gross_exposure.index, gross_exposure.values, label="Gross Exposure", color="black", linewidth=1)
488-
ax.plot(net_exposure.index, net_exposure.values, label="Net Exposure", color=self.COLORS["strategy"], linewidth=1, linestyle="--")
489-
490-
ax.fill_between(net_exposure.index, net_exposure.values, 0, alpha=0.1, color=self.COLORS["strategy"])
507+
ax.fill_between(
508+
net_exposure.index,
509+
net_exposure.values,
510+
0,
511+
alpha=0.1,
512+
color=self.COLORS["strategy"],
513+
)
491514

492515
ax.set_xlabel("Date")
493516
ax.set_ylabel("Exposure")
494517
ax.set_title("Portfolio Exposure")
495518
ax.legend(loc="upper left")
496-
519+
497520
return ax
498521

499522
def plot_top_positions(self, top_n: int = 10, ax: Optional[Any] = None) -> Any:
@@ -508,84 +531,105 @@ def plot_top_positions(self, top_n: int = 10, ax: Optional[Any] = None) -> Any:
508531
fig, ax = plt.subplots(figsize=(10, 6))
509532

510533
# Calculate average absolute allocation
511-
avg_allocation = self.positions.abs().mean().sort_values(ascending=False).head(top_n)
512-
513-
sns.barplot(x=avg_allocation.values, y=avg_allocation.index, ax=ax, palette="viridis", orient="h")
514-
534+
avg_allocation = (
535+
self.positions.abs().mean().sort_values(ascending=False).head(top_n)
536+
)
537+
538+
sns.barplot(
539+
x=avg_allocation.values,
540+
y=avg_allocation.index,
541+
ax=ax,
542+
palette="viridis",
543+
orient="h",
544+
)
545+
515546
ax.set_xlabel("Average Absolute Allocation")
516547
ax.set_title(f"Top {top_n} Positions by Allocation")
517-
548+
518549
return ax
519550

520551
def plot_trade_pnl_distribution(self, ax: Optional[Any] = None) -> Any:
521552
"""Plot distribution of trade P&L."""
522553
if not HAS_MATPLOTLIB:
523554
raise ImportError("matplotlib required for plotting")
524-
555+
525556
if self.transactions is None or "pnl" not in self.transactions.columns:
526557
return None
527-
558+
528559
if ax is None:
529560
fig, ax = plt.subplots(figsize=(10, 6))
530-
561+
531562
pnls = self.transactions["pnl"].dropna()
532563
if pnls.empty:
533564
return None
534-
565+
535566
sns.histplot(pnls, kde=True, ax=ax, color=self.COLORS["strategy"])
536567
ax.axvline(0, color="black", linestyle="--", linewidth=1)
537-
568+
538569
ax.set_xlabel("Trade P&L")
539570
ax.set_title("Trade P&L Distribution")
540-
571+
541572
# Add stats
542573
win_rate = (pnls > 0).mean()
543574
avg_win = pnls[pnls > 0].mean() if not pnls[pnls > 0].empty else 0
544575
avg_loss = pnls[pnls < 0].mean() if not pnls[pnls < 0].empty else 0
545-
profit_factor = abs(pnls[pnls > 0].sum() / pnls[pnls < 0].sum()) if pnls[pnls < 0].sum() != 0 else float('inf')
546-
576+
profit_factor = (
577+
abs(pnls[pnls > 0].sum() / pnls[pnls < 0].sum())
578+
if pnls[pnls < 0].sum() != 0
579+
else float("inf")
580+
)
581+
547582
text = f"Win Rate: {win_rate:.1%}\nAvg Win: ${avg_win:.2f}\nAvg Loss: ${avg_loss:.2f}\nProfit Factor: {profit_factor:.2f}"
548583
self._create_text_box(ax, text, (0.05, 0.95))
549-
584+
550585
return ax
551586

552-
def plot_bayesian_cone(self, ax: Optional[Any] = None, n_samples: int = 1000) -> Any:
587+
def plot_bayesian_cone(
588+
self, ax: Optional[Any] = None, n_samples: int = 1000
589+
) -> Any:
553590
"""Plot Bayesian cone for Sharpe ratio uncertainty."""
554591
if not HAS_MATPLOTLIB:
555592
raise ImportError("matplotlib required for plotting")
556-
557-
593+
558594
if ax is None:
559595
fig, ax = plt.subplots(figsize=(12, 6))
560-
596+
561597
# Bootstrap resampling for Sharpe ratio distribution
562598
sharpe_dist = []
563599
returns_array = self.returns.values
564-
600+
565601
for _ in range(n_samples):
566602
# Simple bootstrap
567-
sample = np.random.choice(returns_array, size=len(returns_array), replace=True)
603+
sample = np.random.choice(
604+
returns_array, size=len(returns_array), replace=True
605+
)
568606
if np.std(sample) > 0:
569-
sharpe = np.mean(sample) / np.std(sample) * np.sqrt(self.periods_per_year)
607+
sharpe = (
608+
np.mean(sample) / np.std(sample) * np.sqrt(self.periods_per_year)
609+
)
570610
sharpe_dist.append(sharpe)
571-
611+
572612
sharpe_dist = np.array(sharpe_dist)
573613
mean_sharpe = np.mean(sharpe_dist)
574-
614+
575615
# Plot cone
576616
# We project the uncertainty into the future
577617
# This is a simplified visualization - typically cones are for cumulative returns
578618
# Here we plot the distribution of likely Sharpe ratios
579-
619+
580620
sns.histplot(sharpe_dist, kde=True, ax=ax, color=self.COLORS["strategy"])
581-
ax.axvline(mean_sharpe, color="red", linestyle="--", label=f"Mean: {mean_sharpe:.2f}")
582-
ax.axvline(np.percentile(sharpe_dist, 5), color="orange", linestyle=":", label="95% CI")
621+
ax.axvline(
622+
mean_sharpe, color="red", linestyle="--", label=f"Mean: {mean_sharpe:.2f}"
623+
)
624+
ax.axvline(
625+
np.percentile(sharpe_dist, 5), color="orange", linestyle=":", label="95% CI"
626+
)
583627
ax.axvline(np.percentile(sharpe_dist, 95), color="orange", linestyle=":")
584-
628+
585629
ax.set_title(f"Bootstrap Sharpe Ratio Distribution (N={n_samples})")
586630
ax.set_xlabel("Sharpe Ratio")
587631
ax.legend()
588-
632+
589633
return ax
590634

591635
# =========================================================================
@@ -707,97 +751,109 @@ def create_full_tear_sheet(
707751
plt.show()
708752

709753
return fig
710-
711-
def create_position_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
754+
755+
def create_position_tear_sheet(
756+
self, filename: Optional[str] = None, show: bool = True
757+
) -> Optional[Any]:
712758
"""Create position analysis tear sheet."""
713759
if not HAS_MATPLOTLIB:
714760
raise ImportError("matplotlib required for tear sheets")
715761

716762
self._setup_plot_style()
717-
763+
718764
fig = plt.figure(figsize=(14, 12))
719765
gs = gridspec.GridSpec(3, 1, figure=fig, hspace=0.4)
720-
766+
721767
# Exposure
722768
ax1 = fig.add_subplot(gs[0, :])
723769
self.plot_position_exposure(ax1)
724-
770+
725771
# Top Positions
726772
ax2 = fig.add_subplot(gs[1, :])
727773
self.plot_top_positions(ax=ax2)
728-
774+
729775
# Returns (for context)
730776
ax3 = fig.add_subplot(gs[2, :])
731777
self.plot_cumulative_returns(ax3)
732-
733-
plt.suptitle("Position Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
734-
778+
779+
plt.suptitle(
780+
"Position Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01
781+
)
782+
735783
if filename:
736784
fig.savefig(filename, dpi=150, bbox_inches="tight")
737-
785+
738786
if show:
739787
plt.show()
740-
788+
741789
return fig
742790

743-
def create_round_trip_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
791+
def create_round_trip_tear_sheet(
792+
self, filename: Optional[str] = None, show: bool = True
793+
) -> Optional[Any]:
744794
"""Create trade analysis tear sheet."""
745795
if not HAS_MATPLOTLIB:
746796
raise ImportError("matplotlib required for tear sheets")
747797

748798
self._setup_plot_style()
749-
799+
750800
fig = plt.figure(figsize=(14, 12))
751801
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.4, wspace=0.3)
752-
802+
753803
# Trade P&L Distribution
754804
ax1 = fig.add_subplot(gs[0, 0])
755805
self.plot_trade_pnl_distribution(ax1)
756-
806+
757807
# Cumulative Returns (for context)
758808
ax2 = fig.add_subplot(gs[0, 1])
759809
self.plot_cumulative_returns(ax2)
760-
810+
761811
# Rolling Sharpe
762812
ax3 = fig.add_subplot(gs[1, :])
763813
self.plot_rolling_sharpe(ax=ax3)
764-
765-
plt.suptitle("Trade Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
766-
814+
815+
plt.suptitle(
816+
"Trade Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01
817+
)
818+
767819
if filename:
768820
fig.savefig(filename, dpi=150, bbox_inches="tight")
769-
821+
770822
if show:
771823
plt.show()
772-
824+
773825
return fig
774-
775-
def create_bayesian_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
826+
827+
def create_bayesian_tear_sheet(
828+
self, filename: Optional[str] = None, show: bool = True
829+
) -> Optional[Any]:
776830
"""Create Bayesian analysis tear sheet."""
777831
if not HAS_MATPLOTLIB:
778832
raise ImportError("matplotlib required for tear sheets")
779833

780834
self._setup_plot_style()
781-
835+
782836
fig = plt.figure(figsize=(14, 10))
783837
gs = gridspec.GridSpec(2, 1, figure=fig, hspace=0.4)
784-
838+
785839
# Bayesian Cone / Sharpe Distribution
786840
ax1 = fig.add_subplot(gs[0, :])
787841
self.plot_bayesian_cone(ax1)
788-
842+
789843
# Returns Distribution (for context)
790844
ax2 = fig.add_subplot(gs[1, :])
791845
self.plot_returns_distribution(ax2)
792-
793-
plt.suptitle("Bayesian Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
794-
846+
847+
plt.suptitle(
848+
"Bayesian Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01
849+
)
850+
795851
if filename:
796852
fig.savefig(filename, dpi=150, bbox_inches="tight")
797-
853+
798854
if show:
799855
plt.show()
800-
856+
801857
return fig
802858

803859
def get_metrics_summary(self) -> pd.DataFrame:

0 commit comments

Comments
 (0)