Skip to content

Commit 9797c8f

Browse files
Enhance Technical Indicators and Portfolio Optimization
- Implemented Nested Clustered Optimization (NCO) - Greatly enhanced existing technical indicators (KAMA, ADX, Supertrend, etc.) - Integrated TA-Lib support for improved performance and accuracy - Added new indicators: HMA, ALMA, Coppock Curve, Vortex, Mass Index, etc. - Standardized indicators with Wilder's smoothing where appropriate - Unified and improved the core API for technical analysis
1 parent 8d618ee commit 9797c8f

File tree

16 files changed

+948
-357
lines changed

16 files changed

+948
-357
lines changed

meridianalgo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
MeridianAlgo v6.2.1 - The Complete Quantitative Finance Platform
2+
MeridianAlgo v6.2.2 - The Complete Quantitative Finance Platform
33
44
A comprehensive, institutional-grade Python library for quantitative finance
55
covering everything from trading research to portfolio analytics to derivatives.

meridianalgo/analytics/tear_sheets.py

Lines changed: 224 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,132 @@ def plot_yearly_returns(self, ax: Optional[Any] = None) -> Any:
462462

463463
return ax
464464

465+
def plot_position_exposure(self, ax: Optional[Any] = None) -> Any:
466+
"""Plot gross and net exposure over time."""
467+
if not HAS_MATPLOTLIB:
468+
raise ImportError("matplotlib required for plotting")
469+
470+
if self.positions is None:
471+
return None
472+
473+
if ax is None:
474+
fig, ax = plt.subplots(figsize=(12, 4))
475+
476+
# Calculate exposures if not provided
477+
if self.gross_lev is None:
478+
long_exposure = self.positions[self.positions > 0].sum(axis=1)
479+
short_exposure = self.positions[self.positions < 0].sum(axis=1)
480+
gross_exposure = long_exposure + short_exposure.abs()
481+
net_exposure = long_exposure + short_exposure
482+
else:
483+
gross_exposure = self.gross_lev
484+
# 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)
486+
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"])
491+
492+
ax.set_xlabel("Date")
493+
ax.set_ylabel("Exposure")
494+
ax.set_title("Portfolio Exposure")
495+
ax.legend(loc="upper left")
496+
497+
return ax
498+
499+
def plot_top_positions(self, top_n: int = 10, ax: Optional[Any] = None) -> Any:
500+
"""Plot top positions by allocation."""
501+
if not HAS_MATPLOTLIB:
502+
raise ImportError("matplotlib required for plotting")
503+
504+
if self.positions is None or self.positions.empty:
505+
return None
506+
507+
if ax is None:
508+
fig, ax = plt.subplots(figsize=(10, 6))
509+
510+
# 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+
515+
ax.set_xlabel("Average Absolute Allocation")
516+
ax.set_title(f"Top {top_n} Positions by Allocation")
517+
518+
return ax
519+
520+
def plot_trade_pnl_distribution(self, ax: Optional[Any] = None) -> Any:
521+
"""Plot distribution of trade P&L."""
522+
if not HAS_MATPLOTLIB:
523+
raise ImportError("matplotlib required for plotting")
524+
525+
if self.transactions is None or "pnl" not in self.transactions.columns:
526+
return None
527+
528+
if ax is None:
529+
fig, ax = plt.subplots(figsize=(10, 6))
530+
531+
pnls = self.transactions["pnl"].dropna()
532+
if pnls.empty:
533+
return None
534+
535+
sns.histplot(pnls, kde=True, ax=ax, color=self.COLORS["strategy"])
536+
ax.axvline(0, color="black", linestyle="--", linewidth=1)
537+
538+
ax.set_xlabel("Trade P&L")
539+
ax.set_title("Trade P&L Distribution")
540+
541+
# Add stats
542+
win_rate = (pnls > 0).mean()
543+
avg_win = pnls[pnls > 0].mean() if not pnls[pnls > 0].empty else 0
544+
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+
547+
text = f"Win Rate: {win_rate:.1%}\nAvg Win: ${avg_win:.2f}\nAvg Loss: ${avg_loss:.2f}\nProfit Factor: {profit_factor:.2f}"
548+
self._create_text_box(ax, text, (0.05, 0.95))
549+
550+
return ax
551+
552+
def plot_bayesian_cone(self, ax: Optional[Any] = None, n_samples: int = 1000) -> Any:
553+
"""Plot Bayesian cone for Sharpe ratio uncertainty."""
554+
if not HAS_MATPLOTLIB:
555+
raise ImportError("matplotlib required for plotting")
556+
557+
558+
if ax is None:
559+
fig, ax = plt.subplots(figsize=(12, 6))
560+
561+
# Bootstrap resampling for Sharpe ratio distribution
562+
sharpe_dist = []
563+
returns_array = self.returns.values
564+
565+
for _ in range(n_samples):
566+
# Simple bootstrap
567+
sample = np.random.choice(returns_array, size=len(returns_array), replace=True)
568+
if np.std(sample) > 0:
569+
sharpe = np.mean(sample) / np.std(sample) * np.sqrt(self.periods_per_year)
570+
sharpe_dist.append(sharpe)
571+
572+
sharpe_dist = np.array(sharpe_dist)
573+
mean_sharpe = np.mean(sharpe_dist)
574+
575+
# Plot cone
576+
# We project the uncertainty into the future
577+
# This is a simplified visualization - typically cones are for cumulative returns
578+
# Here we plot the distribution of likely Sharpe ratios
579+
580+
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")
583+
ax.axvline(np.percentile(sharpe_dist, 95), color="orange", linestyle=":")
584+
585+
ax.set_title(f"Bootstrap Sharpe Ratio Distribution (N={n_samples})")
586+
ax.set_xlabel("Sharpe Ratio")
587+
ax.legend()
588+
589+
return ax
590+
465591
# =========================================================================
466592
# FULL TEAR SHEETS
467593
# =========================================================================
@@ -581,6 +707,98 @@ def create_full_tear_sheet(
581707
plt.show()
582708

583709
return fig
710+
711+
def create_position_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
712+
"""Create position analysis tear sheet."""
713+
if not HAS_MATPLOTLIB:
714+
raise ImportError("matplotlib required for tear sheets")
715+
716+
self._setup_plot_style()
717+
718+
fig = plt.figure(figsize=(14, 12))
719+
gs = gridspec.GridSpec(3, 1, figure=fig, hspace=0.4)
720+
721+
# Exposure
722+
ax1 = fig.add_subplot(gs[0, :])
723+
self.plot_position_exposure(ax1)
724+
725+
# Top Positions
726+
ax2 = fig.add_subplot(gs[1, :])
727+
self.plot_top_positions(ax=ax2)
728+
729+
# Returns (for context)
730+
ax3 = fig.add_subplot(gs[2, :])
731+
self.plot_cumulative_returns(ax3)
732+
733+
plt.suptitle("Position Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
734+
735+
if filename:
736+
fig.savefig(filename, dpi=150, bbox_inches="tight")
737+
738+
if show:
739+
plt.show()
740+
741+
return fig
742+
743+
def create_round_trip_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
744+
"""Create trade analysis tear sheet."""
745+
if not HAS_MATPLOTLIB:
746+
raise ImportError("matplotlib required for tear sheets")
747+
748+
self._setup_plot_style()
749+
750+
fig = plt.figure(figsize=(14, 12))
751+
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.4, wspace=0.3)
752+
753+
# Trade P&L Distribution
754+
ax1 = fig.add_subplot(gs[0, 0])
755+
self.plot_trade_pnl_distribution(ax1)
756+
757+
# Cumulative Returns (for context)
758+
ax2 = fig.add_subplot(gs[0, 1])
759+
self.plot_cumulative_returns(ax2)
760+
761+
# Rolling Sharpe
762+
ax3 = fig.add_subplot(gs[1, :])
763+
self.plot_rolling_sharpe(ax=ax3)
764+
765+
plt.suptitle("Trade Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
766+
767+
if filename:
768+
fig.savefig(filename, dpi=150, bbox_inches="tight")
769+
770+
if show:
771+
plt.show()
772+
773+
return fig
774+
775+
def create_bayesian_tear_sheet(self, filename: Optional[str] = None, show: bool = True) -> Optional[Any]:
776+
"""Create Bayesian analysis tear sheet."""
777+
if not HAS_MATPLOTLIB:
778+
raise ImportError("matplotlib required for tear sheets")
779+
780+
self._setup_plot_style()
781+
782+
fig = plt.figure(figsize=(14, 10))
783+
gs = gridspec.GridSpec(2, 1, figure=fig, hspace=0.4)
784+
785+
# Bayesian Cone / Sharpe Distribution
786+
ax1 = fig.add_subplot(gs[0, :])
787+
self.plot_bayesian_cone(ax1)
788+
789+
# Returns Distribution (for context)
790+
ax2 = fig.add_subplot(gs[1, :])
791+
self.plot_returns_distribution(ax2)
792+
793+
plt.suptitle("Bayesian Analysis Tear Sheet", fontsize=14, fontweight="bold", y=1.01)
794+
795+
if filename:
796+
fig.savefig(filename, dpi=150, bbox_inches="tight")
797+
798+
if show:
799+
plt.show()
800+
801+
return fig
584802

585803
def get_metrics_summary(self) -> pd.DataFrame:
586804
"""Get summary metrics as DataFrame."""
@@ -697,9 +915,7 @@ def create_position_tear_sheet(
697915
matplotlib figure
698916
"""
699917
ts = TearSheet(returns, positions=positions, **kwargs)
700-
# For now, just use full tear sheet
701-
# TODO: Add position-specific plots
702-
return ts.create_full_tear_sheet()
918+
return ts.create_position_tear_sheet()
703919

704920

705921
def create_round_trip_tear_sheet(
@@ -717,8 +933,7 @@ def create_round_trip_tear_sheet(
717933
matplotlib figure
718934
"""
719935
ts = TearSheet(returns, transactions=transactions, **kwargs)
720-
# TODO: Add round-trip specific analysis
721-
return ts.create_full_tear_sheet()
936+
return ts.create_round_trip_tear_sheet()
722937

723938

724939
def create_bayesian_tear_sheet(
@@ -739,6 +954,8 @@ def create_bayesian_tear_sheet(
739954
Returns:
740955
matplotlib figure
741956
"""
742-
# TODO: Implement Bayesian analysis
743957
ts = TearSheet(returns, benchmark=benchmark, **kwargs)
744-
return ts.create_full_tear_sheet()
958+
# Pass n_samples logic if method supported it, but plot_bayesian_cone does.
959+
# Note: create_bayesian_tear_sheet in class doesn't accept n_samples, so we rely on default or modify
960+
# Let's modify the class method call if possible, or just accept the default.
961+
return ts.create_bayesian_tear_sheet()

meridianalgo/backtesting/backtester.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,15 @@ def get_position(self, symbol: str) -> Position:
126126
self.positions[symbol] = Position(symbol)
127127
return self.positions[symbol]
128128

129-
def process_fill(self, fill_event: FillEvent) -> None:
130-
"""Process a fill event and update portfolio."""
129+
def process_fill(self, fill_event: FillEvent) -> float:
130+
"""
131+
Process a fill event and update portfolio.
132+
133+
Returns:
134+
Realized P&L from the fill
135+
"""
131136
if fill_event.fill_status == FillStatus.REJECTED:
132-
return
137+
return 0.0
133138

134139
position = self.get_position(fill_event.symbol)
135140

@@ -146,6 +151,8 @@ def process_fill(self, fill_event: FillEvent) -> None:
146151
realized_pnl = position.add_fill(fill_event)
147152
self.total_realized_pnl += realized_pnl
148153
self.total_commission += fill_event.commission
154+
155+
return realized_pnl
149156

150157
def update_market_values(self, market_prices: Dict[str, float]) -> None:
151158
"""Update market values for all positions."""
@@ -382,6 +389,7 @@ def __init__(
382389

383390
# Tracking
384391
self.portfolio_history: List[Dict[str, Any]] = []
392+
self.position_history_list: List[Dict[str, Any]] = []
385393
self.trade_history: List[Dict[str, Any]] = []
386394
self.current_date: Optional[datetime] = None
387395

@@ -473,22 +481,28 @@ def _handle_order_event(self, event: OrderEvent) -> None:
473481

474482
def _handle_fill_event(self, event: FillEvent) -> None:
475483
"""Handle fill event by updating portfolio."""
476-
self.portfolio.process_fill(event)
484+
realized_pnl = self.portfolio.process_fill(event)
477485

478486
# Notify strategy
479487
if self.strategy:
480488
self.strategy.on_fill_event(event)
481489

482490
# Record trade
483-
self._record_trade(event)
491+
self._record_trade(event, realized_pnl)
484492

485493
def _record_portfolio_state(self) -> None:
486494
"""Record current portfolio state."""
487495
summary = self.portfolio.get_portfolio_summary()
488496
summary["timestamp"] = self.current_date
489497
self.portfolio_history.append(summary)
490-
491-
def _record_trade(self, fill_event: FillEvent) -> None:
498+
499+
# Record positions
500+
positions_snapshot = {"timestamp": self.current_date}
501+
for symbol, pos in self.portfolio.positions.items():
502+
positions_snapshot[symbol] = pos.quantity
503+
self.position_history_list.append(positions_snapshot)
504+
505+
def _record_trade(self, fill_event: FillEvent, realized_pnl: float = 0.0) -> None:
492506
"""Record trade details."""
493507
if fill_event.fill_status != FillStatus.REJECTED:
494508
trade = {
@@ -499,6 +513,7 @@ def _record_trade(self, fill_event: FillEvent) -> None:
499513
"price": fill_event.fill_price,
500514
"commission": fill_event.commission,
501515
"order_id": fill_event.order_id,
516+
"pnl": realized_pnl,
502517
}
503518
self.trade_history.append(trade)
504519

@@ -540,6 +555,11 @@ def _calculate_results(
540555
portfolio_df = pd.DataFrame(self.portfolio_history)
541556
if len(portfolio_df) > 0:
542557
portfolio_df.set_index("timestamp", inplace=True)
558+
559+
position_history = pd.DataFrame(self.position_history_list)
560+
if len(position_history) > 0:
561+
position_history.set_index("timestamp", inplace=True)
562+
position_history = position_history.fillna(0)
543563

544564
trade_df = pd.DataFrame(self.trade_history)
545565

@@ -590,13 +610,13 @@ def _calculate_results(
590610

591611
# Trade statistics
592612
total_trades = len(trade_df)
593-
if total_trades > 0:
594-
# Calculate P&L per trade (simplified)
595-
winning_trades = 0 # Would need more complex calculation
596-
losing_trades = 0
597-
else:
598-
winning_trades = 0
599-
losing_trades = 0
613+
winning_trades = 0
614+
losing_trades = 0
615+
616+
if total_trades > 0 and "pnl" in trade_df.columns:
617+
# We count transactions with realized P&L as "trades" for win/loss stats
618+
winning_trades = len(trade_df[trade_df["pnl"] > 0])
619+
losing_trades = len(trade_df[trade_df["pnl"] < 0])
600620

601621
execution_time = datetime.now() - start_time
602622

@@ -617,7 +637,7 @@ def _calculate_results(
617637
calmar_ratio=calmar_ratio,
618638
portfolio_history=portfolio_df,
619639
trade_history=trade_df,
620-
position_history=pd.DataFrame(), # Would be implemented
640+
position_history=position_history,
621641
daily_returns=daily_returns,
622642
cumulative_returns=cumulative_returns,
623643
drawdowns=drawdown,

0 commit comments

Comments
 (0)