|
| 1 | +#!/usr/bin/env python |
| 2 | +"""TradeLogger Observer Module - Comprehensive trade and data logging. |
| 3 | +
|
| 4 | +This module provides the TradeLogger observer for recording order, trade, |
| 5 | +position, and bar data (OHLCV + open interest + custom fields) during |
| 6 | +backtesting. All logs are stored in accessible lists and populated in |
| 7 | +real-time as each bar is processed. |
| 8 | +
|
| 9 | +Classes: |
| 10 | + TradeLogger: Observer that logs orders, trades, positions, and bar data. |
| 11 | +
|
| 12 | +Example: |
| 13 | + >>> cerebro = bt.Cerebro() |
| 14 | + >>> cerebro.addobserver(bt.observers.TradeLogger) |
| 15 | + >>> results = cerebro.run() |
| 16 | + >>> strat = results[0] |
| 17 | + >>> tl = strat.stats.tradelogger |
| 18 | + >>> print(tl.order_log) |
| 19 | + >>> print(tl.trade_log) |
| 20 | + >>> print(tl.position_log) |
| 21 | + >>> print(tl.data_log) |
| 22 | +""" |
| 23 | + |
| 24 | +from ..observer import Observer |
| 25 | +from ..trade import Trade |
| 26 | +from ..utils.date import num2date |
| 27 | + |
| 28 | + |
| 29 | +class TradeLogger(Observer): |
| 30 | + """Observer that logs orders, trades, positions, and bar data. |
| 31 | +
|
| 32 | + Records comprehensive information during backtesting in real-time: |
| 33 | + - **order_log**: Order events (ref, type, status, size, price, etc.) |
| 34 | + - **trade_log**: Trade events (ref, status, size, price, pnl, etc.) |
| 35 | + - **position_log**: Position snapshot per bar (size, price) |
| 36 | + - **data_log**: Bar data per bar (datetime, OHLCV, open interest, |
| 37 | + and any extra lines defined on the data feed) |
| 38 | +
|
| 39 | + Logs are populated incrementally on each bar, so they are accessible |
| 40 | + during the run (e.g., from the strategy's next() method). |
| 41 | +
|
| 42 | + Params: |
| 43 | + - ``log_orders`` (default: ``True``): Whether to log order events. |
| 44 | + - ``log_trades`` (default: ``True``): Whether to log trade events. |
| 45 | + - ``log_positions`` (default: ``True``): Whether to log position snapshots. |
| 46 | + - ``log_data`` (default: ``True``): Whether to log bar data. |
| 47 | + - ``extra_fields`` (default: ``None``): Additional field names to |
| 48 | + extract from the data feed lines beyond the standard OHLCV + |
| 49 | + open interest. If None, all extra lines on the data feed are |
| 50 | + automatically included. |
| 51 | +
|
| 52 | + Example: |
| 53 | + >>> cerebro = bt.Cerebro() |
| 54 | + >>> cerebro.addobserver(bt.observers.TradeLogger) |
| 55 | + >>> results = cerebro.run() |
| 56 | + >>> strat = results[0] |
| 57 | + >>> tl = strat.stats.tradelogger |
| 58 | + >>> all_logs = tl.get_all_logs() |
| 59 | + """ |
| 60 | + |
| 61 | + _stclock = True |
| 62 | + |
| 63 | + lines = ("dummy",) |
| 64 | + |
| 65 | + params = ( |
| 66 | + ("log_orders", True), |
| 67 | + ("log_trades", True), |
| 68 | + ("log_positions", True), |
| 69 | + ("log_data", True), |
| 70 | + ("extra_fields", None), |
| 71 | + ) |
| 72 | + |
| 73 | + plotinfo = dict(plot=False, subplot=False) |
| 74 | + |
| 75 | + def __init__(self): |
| 76 | + """Initialize TradeLogger with empty log lists.""" |
| 77 | + self.order_log = [] |
| 78 | + self.trade_log = [] |
| 79 | + self.position_log = [] |
| 80 | + self.data_log = [] |
| 81 | + |
| 82 | + @property |
| 83 | + def _owner_datas(self): |
| 84 | + """Get the strategy owner's data feeds. |
| 85 | +
|
| 86 | + Observers have _mindatas=0 so self.datas/self.ddatas are empty. |
| 87 | + Strategy-wide observers must access data through the owner. |
| 88 | + """ |
| 89 | + if hasattr(self, '_owner') and self._owner is not None: |
| 90 | + return getattr(self._owner, 'datas', []) |
| 91 | + return [] |
| 92 | + |
| 93 | + def next(self): |
| 94 | + """Record order, trade, position, and bar data for the current bar. |
| 95 | +
|
| 96 | + Called on every bar during the run, so logs are available in real-time. |
| 97 | + """ |
| 98 | + if self.p.log_orders: |
| 99 | + self._log_orders() |
| 100 | + |
| 101 | + if self.p.log_trades: |
| 102 | + self._log_trades() |
| 103 | + |
| 104 | + if self.p.log_positions: |
| 105 | + self._log_positions() |
| 106 | + |
| 107 | + if self.p.log_data: |
| 108 | + self._log_data() |
| 109 | + |
| 110 | + def _log_orders(self): |
| 111 | + """Log pending order events for the current bar.""" |
| 112 | + for order in self._owner._orderspending: |
| 113 | + entry = dict( |
| 114 | + ref=order.ref, |
| 115 | + ordtype=order.OrdTypes[order.ordtype] if order.ordtype is not None else "Unknown", |
| 116 | + status=order.Status[order.status], |
| 117 | + size=order.size, |
| 118 | + price=order.created.price if order.created else None, |
| 119 | + exectype=order.ExecTypes[order.exectype] if order.exectype is not None else None, |
| 120 | + executed_price=order.executed.price if order.executed and order.executed.size else None, |
| 121 | + executed_size=order.executed.size if order.executed else None, |
| 122 | + commission=order.executed.comm if order.executed else None, |
| 123 | + dt=num2date(order.data.datetime[0]) if len(order.data) else None, |
| 124 | + data_name=getattr(order.data, "_name", ""), |
| 125 | + ) |
| 126 | + self.order_log.append(entry) |
| 127 | + |
| 128 | + def _log_trades(self): |
| 129 | + """Log pending trade events for the current bar.""" |
| 130 | + for trade in self._owner._tradespending: |
| 131 | + entry = dict( |
| 132 | + ref=trade.ref, |
| 133 | + status=Trade.status_names[trade.status], |
| 134 | + size=trade.size, |
| 135 | + price=trade.price, |
| 136 | + value=trade.value, |
| 137 | + commission=trade.commission, |
| 138 | + pnl=trade.pnl, |
| 139 | + pnlcomm=trade.pnlcomm, |
| 140 | + isopen=trade.isopen, |
| 141 | + isclosed=trade.isclosed, |
| 142 | + justopened=trade.justopened, |
| 143 | + baropen=trade.baropen, |
| 144 | + barclose=trade.barclose, |
| 145 | + barlen=trade.barlen, |
| 146 | + dtopen=num2date(trade.dtopen) if trade.dtopen else None, |
| 147 | + dtclose=num2date(trade.dtclose) if trade.dtclose else None, |
| 148 | + data_name=getattr(trade.data, "_name", ""), |
| 149 | + tradeid=trade.tradeid, |
| 150 | + long=trade.long, |
| 151 | + ) |
| 152 | + self.trade_log.append(entry) |
| 153 | + |
| 154 | + def _log_positions(self): |
| 155 | + """Log position snapshot for each data feed in the current bar.""" |
| 156 | + for data in self._owner_datas: |
| 157 | + position = self._owner.getposition(data) |
| 158 | + entry = dict( |
| 159 | + dt=num2date(data.datetime[0]) if len(data) else None, |
| 160 | + data_name=getattr(data, "_name", ""), |
| 161 | + size=position.size, |
| 162 | + price=position.price, |
| 163 | + ) |
| 164 | + self.position_log.append(entry) |
| 165 | + |
| 166 | + def _log_data(self): |
| 167 | + """Log bar data (OHLCV + open interest + extra fields) for each data feed.""" |
| 168 | + for data in self._owner_datas: |
| 169 | + if not len(data): |
| 170 | + continue |
| 171 | + |
| 172 | + entry = dict( |
| 173 | + dt=num2date(data.datetime[0]), |
| 174 | + data_name=getattr(data, "_name", ""), |
| 175 | + open=data.open[0], |
| 176 | + high=data.high[0], |
| 177 | + low=data.low[0], |
| 178 | + close=data.close[0], |
| 179 | + volume=data.volume[0], |
| 180 | + openinterest=data.openinterest[0], |
| 181 | + ) |
| 182 | + |
| 183 | + # Add extra lines beyond standard OHLCV + openinterest + datetime |
| 184 | + standard_lines = { |
| 185 | + "close", "low", "high", "open", |
| 186 | + "volume", "openinterest", "datetime", |
| 187 | + } |
| 188 | + |
| 189 | + extra_fields = self.p.extra_fields |
| 190 | + all_aliases = data.getlinealiases() |
| 191 | + |
| 192 | + if extra_fields is not None: |
| 193 | + # User-specified extra fields |
| 194 | + for field in extra_fields: |
| 195 | + if hasattr(data.lines, field): |
| 196 | + line = getattr(data.lines, field) |
| 197 | + try: |
| 198 | + entry[field] = line[0] |
| 199 | + except (IndexError, Exception): |
| 200 | + entry[field] = None |
| 201 | + else: |
| 202 | + # Auto-detect all extra lines |
| 203 | + for alias in all_aliases: |
| 204 | + if alias not in standard_lines: |
| 205 | + if hasattr(data.lines, alias): |
| 206 | + line = getattr(data.lines, alias) |
| 207 | + try: |
| 208 | + entry[alias] = line[0] |
| 209 | + except (IndexError, Exception): |
| 210 | + entry[alias] = None |
| 211 | + |
| 212 | + self.data_log.append(entry) |
| 213 | + |
| 214 | + def get_order_log(self): |
| 215 | + """Return the collected order log. |
| 216 | +
|
| 217 | + Returns: |
| 218 | + list: List of order event dictionaries. |
| 219 | + """ |
| 220 | + return self.order_log |
| 221 | + |
| 222 | + def get_trade_log(self): |
| 223 | + """Return the collected trade log. |
| 224 | +
|
| 225 | + Returns: |
| 226 | + list: List of trade event dictionaries. |
| 227 | + """ |
| 228 | + return self.trade_log |
| 229 | + |
| 230 | + def get_position_log(self): |
| 231 | + """Return the collected position log. |
| 232 | +
|
| 233 | + Returns: |
| 234 | + list: List of position snapshot dictionaries. |
| 235 | + """ |
| 236 | + return self.position_log |
| 237 | + |
| 238 | + def get_data_log(self): |
| 239 | + """Return the collected data (bar) log. |
| 240 | +
|
| 241 | + Returns: |
| 242 | + list: List of bar data dictionaries with OHLCV + extra fields. |
| 243 | + """ |
| 244 | + return self.data_log |
| 245 | + |
| 246 | + def get_all_logs(self): |
| 247 | + """Return all logs as a dictionary. |
| 248 | +
|
| 249 | + Returns: |
| 250 | + dict: Dictionary with keys 'orders', 'trades', 'positions', 'data'. |
| 251 | + """ |
| 252 | + return dict( |
| 253 | + orders=self.order_log, |
| 254 | + trades=self.trade_log, |
| 255 | + positions=self.position_log, |
| 256 | + data=self.data_log, |
| 257 | + ) |
0 commit comments