diff --git a/backtester/statistics/__init__.py b/backtester/statistics/__init__.py index 5cfc470..6262f3c 100644 --- a/backtester/statistics/__init__.py +++ b/backtester/statistics/__init__.py @@ -1 +1,2 @@ from .charts import monthly_returns_heatmap, returns_histogram, returns_chart +from .stats import summary diff --git a/backtester/statistics/stats.py b/backtester/statistics/stats.py new file mode 100644 index 0000000..5f2dbfb --- /dev/null +++ b/backtester/statistics/stats.py @@ -0,0 +1,76 @@ +import pandas as pd +import numpy as np + +from ..enums import Order + + +def summary(trade_log, balance): + """Returns a table with summary statistics about the trade log""" + + initial_capital = balance['total capital'].get(0) + trade_log.loc[:, + ('totals', + 'capital')] = (-trade_log['totals']['cost'] * trade_log['totals']['qty']).cumsum() + initial_capital + + daily_returns = balance['% change'] * 100 + + first_leg = trade_log.columns.levels[0][0] + + entry_mask = trade_log[first_leg].eval('(order == @Order.BTO) | (order == @Order.STO)') + entries = trade_log.loc[entry_mask] + exits = trade_log.loc[~entry_mask] + + costs = np.array([]) + for contract in entries[first_leg]['contract']: + entry = entries.loc[entries[first_leg]['contract'] == contract] + exit_ = exits.loc[exits[first_leg]['contract'] == contract] + try: + # Here we assume we are entering only once per contract (i.e both entry and exit_ have only one row) + costs = np.append(costs, (entry['totals']['cost'] * entry['totals']['qty']).values[0] + + (exit_['totals']['cost'] * exit_['totals']['qty']).values[0]) + except IndexError: + continue + + wins = costs < 0 + losses = costs >= 0 + profit_factor = np.sum(wins) / np.sum(losses) + total_trades = len(exits) + win_number = np.sum(wins) + loss_number = total_trades - win_number + win_pct = (win_number / total_trades) * 100 + largest_loss = max(0, np.max(costs)) + avg_profit = np.mean(-costs) + avg_pl = np.mean(daily_returns) + total_pl = (trade_log['totals']['capital'].iloc[-1] / initial_capital) * 100 + + data = [total_trades, win_number, loss_number, win_pct, largest_loss, profit_factor, avg_profit, avg_pl, total_pl] + stats = [ + 'Total trades', 'Number of wins', 'Number of losses', 'Win %', 'Largest loss', 'Profit factor', + 'Average profit', 'Average P&L %', 'Total P&L %' + ] + strat = ['Strategy'] + summary = pd.DataFrame(data, stats, strat) + + # Applies formatters to rows + def format_row_wise(styler, formatters): + for row, row_formatter in formatters.items(): + row_num = styler.index.get_loc(row) + + for col_num in range(len(styler.columns)): + styler._display_funcs[(row_num, col_num)] = row_formatter + + return styler + + formatters = { + "Total trades": lambda x: f"{x:.0f}", + "Number of wins": lambda x: f"{x:.0f}", + "Number of losses": lambda x: f"{x:.0f}", + "Win %": lambda x: f"{x:.2f}%", + "Largest loss": lambda x: f"${x:.2f}", + "Profit factor": lambda x: f"{x:.2f}", + "Average profit": lambda x: f"${x:.2f}", + "Average P&L %": lambda x: f"{x:.2f}%", + "Total P&L %": lambda x: f"{x:.2f}%" + } + + return format_row_wise(summary.style, formatters)