@@ -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%} \n Avg Win: ${ avg_win :.2f} \n Avg Loss: ${ avg_loss :.2f} \n Profit 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