diff --git a/quants_lab/strategy/strategy_analysis.py b/quants_lab/strategy/strategy_analysis.py index f00267c..675b181 100644 --- a/quants_lab/strategy/strategy_analysis.py +++ b/quants_lab/strategy/strategy_analysis.py @@ -11,6 +11,9 @@ class StrategyAnalysis: def __init__(self, positions: pd.DataFrame, candles_df: Optional[pd.DataFrame] = None): self.candles_df = candles_df self.positions = positions + self.candles_df["timestamp"] = pd.to_datetime(self.candles_df["timestamp"], unit="ms") + self.positions["timestamp"] = pd.to_datetime(self.positions["timestamp"], unit="ms") + self.positions["close_time"] = pd.to_datetime(self.positions["close_time"], unit="ms") self.base_figure = None def create_base_figure(self, candlestick=True, volume=True, positions=False, trade_pnl=False, extra_rows=0): @@ -32,18 +35,18 @@ class StrategyAnalysis: def add_positions(self): # Add long and short positions active_signals = self.positions.copy() - active_signals.loc[active_signals['side'] == -1, 'symbol'] = 'triangle-down' - active_signals.loc[active_signals['side'] == 1, 'symbol'] = 'triangle-up' - active_signals.loc[active_signals['real_class'] == 1, 'color'] = 'lightgreen' - active_signals.loc[active_signals['real_class'] == -1, 'color'] = 'red' - self.base_figure.add_trace(go.Scatter(x=active_signals.loc[(active_signals['side'] != 0), 'timestamp'], - y=active_signals.loc[active_signals['side'] != 0, 'close'], - name='Entry Price: $', - mode='markers', - marker_color=active_signals.loc[(active_signals['side'] != 0), 'color'], - marker_symbol=active_signals.loc[(active_signals['side'] != 0), 'symbol'], + active_signals.loc[active_signals["signal"] == -1, "symbol"] = "triangle-down" + active_signals.loc[active_signals["signal"] == 1, "symbol"] = "triangle-up" + active_signals.loc[active_signals["profitable"] == 1, "color"] = "lightgreen" + active_signals.loc[active_signals["profitable"] == -1, "color"] = "red" + self.base_figure.add_trace(go.Scatter(x=active_signals.loc[(active_signals["side"] != 0), "timestamp"], + y=active_signals.loc[active_signals["side"] != 0, "close"], + name="Entry Price: $", + mode="markers", + marker_color=active_signals.loc[(active_signals["side"] != 0), "color"], + marker_symbol=active_signals.loc[(active_signals["side"] != 0), "symbol"], marker_size=20, - marker_line={'color': 'black', 'width': 0.7}), + marker_line={"color": "black", "width": 0.7}), row=1, col=1) for index, row in active_signals.iterrows(): @@ -53,7 +56,7 @@ class StrategyAnalysis: x0=row.timestamp, y0=row.close, x1=row.close_time, - y1=row.tp, + y1=row.take_profit_price, line=dict(color="green"), row=1, col=1) # Add SL @@ -63,7 +66,7 @@ class StrategyAnalysis: x0=row.timestamp, y0=row.close, x1=row.close_time, - y1=row.sl, + y1=row.stop_loss_price, line=dict(color="red"), row=1, col=1) @@ -81,11 +84,11 @@ class StrategyAnalysis: def add_candles_graph(self): self.base_figure.add_trace( go.Candlestick( - x=self.candles_df['timestamp'], - open=self.candles_df['open'], - high=self.candles_df['high'], - low=self.candles_df['low'], - close=self.candles_df['close'], + x=self.candles_df["timestamp"], + open=self.candles_df["open"], + high=self.candles_df["high"], + low=self.candles_df["low"], + close=self.candles_df["close"], name="OHLC" ), row=1, col=1, @@ -94,11 +97,11 @@ class StrategyAnalysis: def add_volume(self): self.base_figure.add_trace( go.Bar( - x=self.candles_df['timestamp'], - y=self.candles_df['volume'], + x=self.candles_df["timestamp"], + y=self.candles_df["volume"], name="Volume", opacity=0.5, - marker=dict(color='lightgreen') + marker=dict(color="lightgreen") ), row=2, col=1, ) @@ -106,23 +109,23 @@ class StrategyAnalysis: def add_trade_pnl(self, row=2): self.base_figure.add_trace( go.Scatter( - x=self.positions['timestamp'], - y=self.positions['ret_usd'].cumsum(), + x=self.positions["timestamp"], + y=self.positions["net_pnl_quote"].cumsum(), name="Cumulative Trade PnL", - mode='lines', - line=dict(color='chocolate', width=2)), + mode="lines", + line=dict(color="chocolate", width=2)), row=row, col=1 ) - self.base_figure.update_yaxes(title_text='Cum Trade PnL', row=row, col=1) + self.base_figure.update_yaxes(title_text="Cum Trade PnL", row=row, col=1) def update_layout(self, volume=True): self.base_figure.update_layout( title={ - 'text': "Backtesting Analysis", - 'y': 0.95, - 'x': 0.5, - 'xanchor': 'center', - 'yanchor': 'top' + "text": "Backtesting Analysis", + "y": 0.95, + "x": 0.5, + "xanchor": "center", + "yanchor": "top" }, legend=dict( orientation="h", @@ -133,7 +136,7 @@ class StrategyAnalysis: ), height=1000, xaxis_rangeslider_visible=False, - hovermode='x unified' + hovermode="x unified" ) self.base_figure.update_yaxes(title_text="Price", row=1, col=1) if volume: @@ -141,10 +144,10 @@ class StrategyAnalysis: self.base_figure.update_xaxes(title_text="Time", row=self.rows, col=1) def initial_portfolio(self): - return self.positions['current_portfolio'].dropna().values[0] + return self.positions["inventory"].dropna().values[0] def final_portfolio(self): - return self.positions['current_portfolio'].dropna().values[-1] + return self.positions["inventory"].dropna().values[-1] def net_profit_usd(self): return self.final_portfolio() - self.initial_portfolio() @@ -153,22 +156,22 @@ class StrategyAnalysis: return self.net_profit_usd() / self.initial_portfolio() def returns(self): - return self.positions['ret_usd'] / self.initial_portfolio() + return self.positions["net_pnl_quote"] / self.initial_portfolio() def total_positions(self): return self.positions.shape[0] - 1 def win_signals(self): - return self.positions.loc[(self.positions['real_class'] > 0) & (self.positions["side"] != 0)] + return self.positions.loc[(self.positions["profitable"] > 0) & (self.positions["side"] != 0)] def loss_signals(self): - return self.positions.loc[(self.positions['real_class'] < 0) & (self.positions["side"] != 0)] + return self.positions.loc[(self.positions["profitable"] < 0) & (self.positions["side"] != 0)] def accuracy(self): return self.win_signals().shape[0] / self.total_positions() def max_drawdown_usd(self): - cumulative_returns = self.positions["ret_usd"].cumsum() + cumulative_returns = self.positions["net_pnl_quote"].cumsum() peak = np.maximum.accumulate(cumulative_returns) drawdown = (cumulative_returns - peak) max_draw_down = np.min(drawdown) @@ -182,25 +185,25 @@ class StrategyAnalysis: return returns.mean() / returns.std() def profit_factor(self): - total_won = self.win_signals().loc[:, 'ret_usd'].sum() - total_loss = - self.loss_signals().loc[:, 'ret_usd'].sum() + total_won = self.win_signals().loc[:, "net_pnl_quote"].sum() + total_loss = - self.loss_signals().loc[:, "net_pnl_quote"].sum() return total_won / total_loss def duration_in_minutes(self): - return (self.positions['timestamp'].iloc[-1] - self.positions['timestamp'].iloc[0]).total_seconds() / 60 + return (self.positions["timestamp"].iloc[-1] - self.positions["timestamp"].iloc[0]).total_seconds() / 60 def avg_trading_time_in_minutes(self): - time_diff_minutes = (pd.to_datetime(self.positions['close_time']) - self.positions['timestamp']).dt.total_seconds() / 60 + time_diff_minutes = (self.positions["close_time"] - self.positions["timestamp"]).dt.total_seconds() / 60 return time_diff_minutes.mean() def start_date(self): - return self.candles_df.timestamp.min() + return pd.to_datetime(self.candles_df.timestamp.min(), unit="ms") def end_date(self): - return self.candles_df.timestamp.max() + return pd.to_datetime(self.candles_df.timestamp.max(), unit="ms") def avg_profit(self): - return self.positions.ret_usd.mean() + return self.positions.net_pnl_quote.mean() def text_report(self): return f""" @@ -221,7 +224,7 @@ Strategy Performance Report: fig = go.Figure() fig.add_trace(go.Scatter(name="PnL Over Time", x=self.positions.index, - y=self.positions.ret_usd.cumsum())) + y=self.positions.net_pnl_quote.cumsum())) # Update layout with the required attributes fig.update_layout( title="PnL Over Time",