Skip to content

Commit 609ac4f

Browse files
committed
update TradeLogger
1 parent e52775b commit 609ac4f

File tree

8 files changed

+681
-32
lines changed

8 files changed

+681
-32
lines changed

.idea/sqldialects.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backtrader/observers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@
2929
from .drawdown import *
3030
from .logreturns import *
3131
from .timereturn import *
32+
from .tradelogger import *
3233
from .trades import *

backtrader/observers/buysell.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ def __init__(self):
5656
5757
Sets up tracking for buy/sell order lengths.
5858
"""
59-
self.curbuylen = None
59+
self.curbuylen = 0
60+
self.curselllen = 0
6061

6162
def next(self):
6263
"""Update buy/sell markers based on executed orders.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)