File size: 8,906 Bytes
63bad2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import os
os.environ.setdefault("MPLBACKEND", "Agg")
from datetime import datetime, timedelta
from io import BytesIO

import backtrader as bt
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd

matplotlib.use("Agg")  # Set the backend to non-interactive

fmp_intervals_to_int = {"1min":1, "5min":5, "15min":15, "1hour":60, "4hour":240}
class TransactionRecorder(bt.Analyzer):
    """ Records all buy/sell transactions with details."""
    def __init__(self):
        self.records = []

    def notify_order(self, order):
        if order.status != order.Completed:
            return

        self.records.append({
            'datetime': self.strategy.data.datetime.datetime(),
            'type': 'BUY' if order.isbuy() else 'SELL',
            'price': order.executed.price,
            'size': order.executed.size,
            'value': order.executed.value,
            'commission': order.executed.comm,
        })

    def get_analysis(self):
        return pd.DataFrame(self.records)

class TradeRecorder(bt.Analyzer):
    """ Records detailed trade information including entry/exit prices and PnL."""
    def __init__(self):
        self.records = []
        self.trade_id = 0

    def notify_trade(self, trade):
        # RECORD ENTRY
        if trade.isopen:
            self.current_entry_price = trade.price
            self.current_size = trade.size
            return

        # RECORD EXIT
        if trade.isclosed:
            self.trade_id += 1

            # Backtrader clears trade.size to 0 on close, so restore saved size
            size = self.current_size  

            # Compute exit price from PnL formula:
            # pnl = (exit - entry) * size
            if size is not None and size != 0:
                exit_price = self.current_entry_price + (trade.pnl / size)
            else:
                exit_price = None

            self.records.append({
                'trade_id': self.trade_id,
                'size': size,
                'entry_price': self.current_entry_price,
                'exit_price': exit_price,
                'bruto_profitloss': trade.pnl,
                'neto_profitloss': trade.pnlcomm,
                'date_open': self._format_dt(trade.dtopen),
                'date_close': self._format_dt(trade.dtclose),
            })

            # Reset after close
            self.current_entry_price = None
            self.current_size = None

    def get_analysis(self):
        return pd.DataFrame(self.records)

    def _format_dt(self, val):
        """Convert a backtrader/matplotlib numeric datetime to a readable string."""
        if val is None:
            return None
        try:
            ord_day = int(val)
            frac = val - ord_day
            dt = datetime.fromordinal(ord_day) + timedelta(days=frac)
            return dt.strftime("%Y-%m-%d %H:%M:%S")
        except Exception as e:
            return "Error formatting dt: " + str(e)


def _fig_to_numpy(fig, dpi=150):
    buf = BytesIO()
    fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
    buf.seek(0)
    img = plt.imread(buf, format="png")
    buf.close()
    return img

#TODO: Dates plotting not working
def plot_bt(figs, symbol, market_name, save_img=True):
    """
    Plot backtrader results with market name in title
    and readable date ticks on all x-axes.
    """
    images = []

    plt.ioff()  # Turn off interactive mode
    for i, fig_list in enumerate(figs):
        for j, fig in enumerate(fig_list):
            # Decorate titles
            if fig.axes:
                main_ax = fig.axes[0]
                current_title = main_ax.get_title()
                new_title = f"{market_name} ({symbol.upper()}) - {current_title or 'Price Chart'}"
                main_ax.set_title(new_title, fontsize=8, fontweight="bold")
                fig.suptitle(
                    f"Trading Strategy Analysis: {market_name}",
                    fontsize=10,
                    fontweight="bold",
                    y=0.98,
                )

            fig.set_size_inches(12, 6)
            fig.autofmt_xdate()
            fig.tight_layout(rect=[0, 0, 1, 0.95])
            # Convert to numpy
            img_data = _fig_to_numpy(fig)
            images.append(img_data)

            if save_img:
                filename = f"plot_{symbol}_{i}_{j}.png"
                fig.savefig(filename, dpi=300, bbox_inches="tight")
                print(f"Chart saved as: {filename}")
            
            plt.close(fig)  # Close the figure to free memory
    return images  

def run_bt(cerebro, 
           tckr_symbl="SPY", 
           replay=False,
           replay_compression=15,
           save_img=False,
           interval=None,
           market_name = "Complete Market Name here",
           initial_capital=10000.0,
           commission=0.001,
           slippage_percent=0.01,
           df=None):
    """
    Run backtrader strategy with enhanced plotting
    
    Args:
        strategy: Backtrader strategy class already init
        tckr_symbl: Stock ticker symbol
        save_img: Whether to save plot images
        initial_capital: Starting capital for the broker
        commission: Commission per share
        slippage_percent: Percent (e.g., 0.01 for 0.01%) applied as slippage
        df: pandas DataFrame with datetime index.
    """

    print(f"Running strategy on: {market_name} ({tckr_symbl.upper()})")
    print("-" * 50)
    print(f"Initial capital: {initial_capital}, Commission: {commission}, Slippage%: {slippage_percent}")
    
    if df is None:
        raise ValueError("df is required. Load data with get_data before calling run_bt.")
    
    if replay:
        print(f"Replaying data on: {market_name} ({tckr_symbl.upper()}) at interval {interval} with compression {replay_compression}")
        data = bt.feeds.PandasData(dataname=df, timeframe=bt.TimeFrame.Minutes, compression=fmp_intervals_to_int[interval])
        cerebro.replaydata(data, timeframe=bt.TimeFrame.Minutes, compression=replay_compression)
    else:
        data = bt.feeds.PandasData(dataname=df)
        cerebro.adddata(data)
    
    # Set initial cash and commission
    initial_cash = float(initial_capital)
    cerebro.broker.setcash(initial_cash)
    cerebro.broker.setcommission(commission=float(commission))
    slippage_decimal = float(slippage_percent) / 100.0
    cerebro.broker.set_slippage_perc(slippage_decimal)
    
    # Add analyzers for better performance metrics
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(TransactionRecorder,  _name='transactions')
    cerebro.addanalyzer(TradeRecorder, _name='trades')
    
    # Print starting conditions
    print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
    

    # Run strategy
    results = cerebro.run()
    
    # Calculate and display results
    final_value = cerebro.broker.getvalue()
    total_return = (final_value - initial_cash) / initial_cash * 100
    
    print(f'Final Portfolio Value: ${final_value:,.2f}')
    print(f'Total Return: {total_return:.2f}%')
    
    # Print analyzer results
    strat = results[0]
    
    try:
        sharpe = strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')
        if sharpe != 'N/A':
            print(f'Sharpe Ratio: {sharpe:.3f}')
        else:
            print('Sharpe Ratio: N/A')
    except:
        print('Sharpe Ratio: N/A')
    
    try:
        max_dd = strat.analyzers.drawdown.get_analysis()['max']['drawdown']
        print(f'Max Drawdown: {max_dd:.2f}%')
    except:
        print('Max Drawdown: N/A')

    # Trades and transaction tables
    df_transactions = strat.analyzers.transactions.get_analysis()
    df_trades = strat.analyzers.trades.get_analysis()
    print(f"Number of Trades: {len(df_trades)}")
    print("-" * 50)
    if not df_transactions.empty:
        print("Transactions logs generated")
    else:
        print("No transactions recorded.")
    if not df_trades.empty:
        print("Trades logs generated")
    else:
        print("No trades recorded.")
    


    
    # Generate plot with market name (disable interactive plotting to avoid GUI in threads)
    fig = []
    try:
        figs = cerebro.plot(
            style='candlestick',
            barstyle='candlestick',
            subplot=True,
            plotabove=False,
            downsample=False,
            plotstyle='multiple',
            iplot=False,
            show=False,
            dpi=120,
        )
        print("How many charts where created: ", len(figs))
        fig = plot_bt(figs, market_name, tckr_symbl, save_img)
    except Exception as e:
        print(f"Plot skipped due to error: {e}")
    
    return final_value, total_return, fig, df_trades, df_transactions