Remove deprecated modules and files related to the backtesting framework, including backtest.py, cli.py, config.py, data.py, intrabar.py, logging_utils.py, market_costs.py, metrics.py, trade.py, and supertrend indicators. Introduce a new structure for the backtesting engine with improved organization and functionality, including a CLI handler, data manager, and reporting capabilities. Update dependencies in pyproject.toml to support the new architecture.
This commit is contained in:
228
engine/reporting.py
Normal file
228
engine/reporting.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Reporting module for backtest results.
|
||||
|
||||
Handles summary printing, CSV exports, and plotting.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import vectorbt as vbt
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from engine.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Reporter:
|
||||
"""Reporter for backtest results with market-specific metrics."""
|
||||
|
||||
def __init__(self, output_dir: str = "backtest_logs"):
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
|
||||
def print_summary(self, result) -> None:
|
||||
"""
|
||||
Print backtest summary to console via logger.
|
||||
|
||||
Args:
|
||||
result: BacktestResult or vbt.Portfolio object
|
||||
"""
|
||||
(portfolio, market_type, leverage, funding_paid,
|
||||
liq_count, liq_loss, adjusted_return) = self._extract_result_data(result)
|
||||
|
||||
# Extract period info
|
||||
idx = portfolio.wrapper.index
|
||||
start_date = idx[0].strftime("%Y-%m-%d")
|
||||
end_date = idx[-1].strftime("%Y-%m-%d")
|
||||
|
||||
# Extract price info
|
||||
close = portfolio.close
|
||||
start_price = close.iloc[0].mean() if hasattr(close.iloc[0], 'mean') else close.iloc[0]
|
||||
end_price = close.iloc[-1].mean() if hasattr(close.iloc[-1], 'mean') else close.iloc[-1]
|
||||
price_change = ((end_price - start_price) / start_price) * 100
|
||||
|
||||
# Extract fees
|
||||
stats = portfolio.stats()
|
||||
total_fees = stats.get('Total Fees Paid', 0)
|
||||
|
||||
raw_return = portfolio.total_return().mean() * 100
|
||||
|
||||
# Build summary
|
||||
summary_lines = [
|
||||
"",
|
||||
"=" * 50,
|
||||
"BACKTEST RESULTS",
|
||||
"=" * 50,
|
||||
f"Market Type: [{market_type.upper()}]",
|
||||
f"Leverage: [{leverage}x]",
|
||||
f"Period: [{start_date} to {end_date}]",
|
||||
f"Price: [{start_price:,.2f} -> {end_price:,.2f} ({price_change:+.2f}%)]",
|
||||
]
|
||||
|
||||
# Show adjusted return if liquidations occurred
|
||||
if liq_count > 0 and adjusted_return is not None:
|
||||
summary_lines.append(f"Raw Return: [%{raw_return:.2f}] (before liq adjustment)")
|
||||
summary_lines.append(f"Adj Return: [%{adjusted_return:.2f}] (after liq losses)")
|
||||
else:
|
||||
summary_lines.append(f"Total Return: [%{raw_return:.2f}]")
|
||||
|
||||
summary_lines.extend([
|
||||
f"Sharpe Ratio: [{portfolio.sharpe_ratio().mean():.2f}]",
|
||||
f"Max Drawdown: [%{portfolio.max_drawdown().mean() * 100:.2f}]",
|
||||
f"Total Trades: [{portfolio.trades.count().mean():.0f}]",
|
||||
f"Win Rate: [%{portfolio.trades.win_rate().mean() * 100:.2f}]",
|
||||
f"Total Fees: [{total_fees:,.2f}]",
|
||||
])
|
||||
|
||||
if funding_paid != 0:
|
||||
summary_lines.append(f"Funding Paid: [{funding_paid:,.2f}]")
|
||||
if liq_count > 0:
|
||||
summary_lines.append(f"Liquidations: [{liq_count}] (${liq_loss:,.2f} margin lost)")
|
||||
|
||||
summary_lines.append("=" * 50)
|
||||
logger.info("\n".join(summary_lines))
|
||||
|
||||
def save_reports(self, result, filename_prefix: str) -> None:
|
||||
"""
|
||||
Save trade log, stats, and liquidation events to CSV files.
|
||||
|
||||
Args:
|
||||
result: BacktestResult or vbt.Portfolio object
|
||||
filename_prefix: Prefix for output filenames
|
||||
"""
|
||||
(portfolio, market_type, leverage, funding_paid,
|
||||
liq_count, liq_loss, adjusted_return) = self._extract_result_data(result)
|
||||
|
||||
# Save trades
|
||||
self._save_csv(
|
||||
data=portfolio.trades.records_readable,
|
||||
path=self.output_dir / f"{filename_prefix}_trades.csv",
|
||||
description="trade log"
|
||||
)
|
||||
|
||||
# Save stats with market-specific additions
|
||||
stats = portfolio.stats()
|
||||
stats['Market Type'] = market_type
|
||||
stats['Leverage'] = leverage
|
||||
stats['Total Funding Paid'] = funding_paid
|
||||
stats['Liquidations'] = liq_count
|
||||
stats['Liquidation Loss'] = liq_loss
|
||||
if adjusted_return is not None:
|
||||
stats['Adjusted Return'] = adjusted_return
|
||||
|
||||
self._save_csv(
|
||||
data=stats,
|
||||
path=self.output_dir / f"{filename_prefix}_stats.csv",
|
||||
description="stats"
|
||||
)
|
||||
|
||||
# Save liquidation events if any
|
||||
if hasattr(result, 'liquidation_events') and result.liquidation_events:
|
||||
liq_df = pd.DataFrame([
|
||||
{
|
||||
'entry_time': e.entry_time,
|
||||
'entry_price': e.entry_price,
|
||||
'liquidation_time': e.liquidation_time,
|
||||
'liquidation_price': e.liquidation_price,
|
||||
'actual_price': e.actual_price,
|
||||
'direction': e.direction,
|
||||
'margin_lost_pct': e.margin_lost_pct
|
||||
}
|
||||
for e in result.liquidation_events
|
||||
])
|
||||
self._save_csv(
|
||||
data=liq_df,
|
||||
path=self.output_dir / f"{filename_prefix}_liquidations.csv",
|
||||
description="liquidation events"
|
||||
)
|
||||
|
||||
def plot(self, portfolio: vbt.Portfolio, show: bool = True) -> None:
|
||||
"""Display portfolio plot."""
|
||||
if show:
|
||||
portfolio.plot().show()
|
||||
|
||||
def plot_wfa(
|
||||
self,
|
||||
wfa_results: pd.DataFrame,
|
||||
stitched_curve: pd.Series | None = None,
|
||||
show: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Plot Walk-Forward Analysis results.
|
||||
|
||||
Args:
|
||||
wfa_results: DataFrame with WFA window results
|
||||
stitched_curve: Stitched out-of-sample equity curve
|
||||
show: Whether to display the plot
|
||||
"""
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
shared_xaxes=False,
|
||||
vertical_spacing=0.1,
|
||||
subplot_titles=(
|
||||
"Walk-Forward Test Scores (Sharpe)",
|
||||
"Stitched Out-of-Sample Equity"
|
||||
)
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=wfa_results['window'],
|
||||
y=wfa_results['test_score'],
|
||||
name="Test Sharpe"
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
if stitched_curve is not None:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=stitched_curve.index,
|
||||
y=stitched_curve.values,
|
||||
name="Equity",
|
||||
mode='lines'
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
fig.update_layout(height=800, title_text="Walk-Forward Analysis Report")
|
||||
|
||||
if show:
|
||||
fig.show()
|
||||
|
||||
def _extract_result_data(self, result) -> tuple:
|
||||
"""
|
||||
Extract data from BacktestResult or raw Portfolio.
|
||||
|
||||
Returns:
|
||||
Tuple of (portfolio, market_type, leverage, funding_paid, liq_count,
|
||||
liq_loss, adjusted_return)
|
||||
"""
|
||||
if hasattr(result, 'portfolio'):
|
||||
return (
|
||||
result.portfolio,
|
||||
result.market_type.value,
|
||||
result.leverage,
|
||||
result.total_funding_paid,
|
||||
result.liquidation_count,
|
||||
getattr(result, 'total_liquidation_loss', 0.0),
|
||||
getattr(result, 'adjusted_return', None)
|
||||
)
|
||||
return (result, "unknown", 1, 0.0, 0, 0.0, None)
|
||||
|
||||
def _save_csv(self, data, path: Path, description: str) -> None:
|
||||
"""
|
||||
Save data to CSV with consistent error handling.
|
||||
|
||||
Args:
|
||||
data: DataFrame or Series to save
|
||||
path: Output file path
|
||||
description: Human-readable description for logging
|
||||
"""
|
||||
try:
|
||||
data.to_csv(path)
|
||||
logger.info("Saved %s to %s", description, path)
|
||||
except Exception as e:
|
||||
logger.error("Could not save %s: %s", description, e)
|
||||
Reference in New Issue
Block a user