-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathbt_stats.py
More file actions
297 lines (224 loc) · 11.2 KB
/
Copy pathbt_stats.py
File metadata and controls
297 lines (224 loc) · 11.2 KB
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""
Backtest fill analytics — wraps the backtester to track maker vs taker fills.
Usage (from prosperity4/):
python bt_stats.py trader_v3.py 0 [--data data] [--no-out] [--match-trades all]
Passes all arguments through to the backtester, then prints a fill breakdown.
No changes to your trader or the backtester source required.
"""
import sys
from collections import defaultdict
from pathlib import Path
# ── Bootstrap: add backtester to path ──────────────────────────────
bt_dir = Path(__file__).parent / "backtester"
sys.path.insert(0, str(bt_dir))
from prosperity3bt import runner
from prosperity3bt.datamodel import Order, Symbol, Trade, TradingState
from prosperity3bt.models import BacktestResult, MarketTrade, TradeMatchingMode, TradeRow
# ── Stats accumulator ──────────────────────────────────────────────
stats = defaultdict(lambda: {
"maker_buys": 0, "maker_sells": 0,
"taker_buys": 0, "taker_sells": 0,
"maker_buy_vol": 0, "maker_sell_vol": 0,
"taker_buy_vol": 0, "taker_sell_vol": 0,
"maker_buy_cost": 0, "maker_sell_cost": 0,
"taker_buy_cost": 0, "taker_sell_cost": 0,
"steps_with_fills": 0,
"total_steps": 0,
"orders_submitted": 0,
"orders_unfilled": 0,
})
# ── Patched matching functions ─────────────────────────────────────
_orig_match_buy = runner.match_buy_order
_orig_match_sell = runner.match_sell_order
_orig_match_orders = runner.match_orders
def _tagged_match_buy(state, data, order, market_trades, mode):
"""Wrapper that tags fills as maker (book) vs taker (market trade)."""
product = order.symbol
qty_before = order.quantity
# --- Phase 1: maker fills (against order book) ---
order_depth = state.order_depths[product]
price_matches = sorted(p for p in order_depth.sell_orders if p <= order.price)
trades = []
for price in price_matches:
volume = min(order.quantity, abs(order_depth.sell_orders[price]))
trades.append(Trade(order.symbol, price, volume, "SUBMISSION", "", state.timestamp))
state.position[order.symbol] = state.position.get(order.symbol, 0) + volume
data.profit_loss[order.symbol] -= price * volume
order_depth.sell_orders[price] += volume
if order_depth.sell_orders[price] == 0:
order_depth.sell_orders.pop(price)
order.quantity -= volume
s = stats[product]
s["maker_buys"] += 1
s["maker_buy_vol"] += volume
s["maker_buy_cost"] += price * volume
if order.quantity == 0:
return trades
# --- Phase 2: taker fills (against market trades) ---
if mode == TradeMatchingMode.none:
return trades
for mt in market_trades:
if (mt.sell_quantity == 0
or mt.trade.price > order.price
or (mt.trade.price == order.price and mode == TradeMatchingMode.worse)):
continue
volume = min(order.quantity, mt.sell_quantity)
trades.append(Trade(order.symbol, order.price, volume, "SUBMISSION", mt.trade.seller, state.timestamp))
state.position[order.symbol] = state.position.get(order.symbol, 0) + volume
data.profit_loss[order.symbol] -= order.price * volume
mt.sell_quantity -= volume
order.quantity -= volume
s = stats[product]
s["taker_buys"] += 1
s["taker_buy_vol"] += volume
s["taker_buy_cost"] += order.price * volume
if order.quantity == 0:
return trades
return trades
def _tagged_match_sell(state, data, order, market_trades, mode):
"""Wrapper that tags fills as maker (book) vs taker (market trade)."""
product = order.symbol
qty_before = abs(order.quantity)
order_depth = state.order_depths[product]
price_matches = sorted((p for p in order_depth.buy_orders if p >= order.price), reverse=True)
trades = []
for price in price_matches:
volume = min(abs(order.quantity), order_depth.buy_orders[price])
trades.append(Trade(order.symbol, price, volume, "", "SUBMISSION", state.timestamp))
state.position[order.symbol] = state.position.get(order.symbol, 0) - volume
data.profit_loss[order.symbol] += price * volume
order_depth.buy_orders[price] -= volume
if order_depth.buy_orders[price] == 0:
order_depth.buy_orders.pop(price)
order.quantity += volume
s = stats[product]
s["maker_sells"] += 1
s["maker_sell_vol"] += volume
s["maker_sell_cost"] += price * volume
if order.quantity == 0:
return trades
if mode == TradeMatchingMode.none:
return trades
for mt in market_trades:
if (mt.buy_quantity == 0
or mt.trade.price < order.price
or (mt.trade.price == order.price and mode == TradeMatchingMode.worse)):
continue
volume = min(abs(order.quantity), mt.buy_quantity)
trades.append(Trade(order.symbol, order.price, volume, mt.trade.buyer, "SUBMISSION", state.timestamp))
state.position[order.symbol] = state.position.get(order.symbol, 0) - volume
data.profit_loss[order.symbol] += order.price * volume
mt.buy_quantity -= volume
order.quantity += volume
s = stats[product]
s["taker_sells"] += 1
s["taker_sell_vol"] += volume
s["taker_sell_cost"] += order.price * volume
if order.quantity == 0:
return trades
return trades
def _tagged_match_orders(state, data, orders, result, mode):
"""Wrapper that tracks per-step fill counts and order submission counts."""
market_trades = {
product: [MarketTrade(t, t.quantity, t.quantity) for t in trades]
for product, trades in data.trades[state.timestamp].items()
}
for product in data.products:
s = stats[product]
s["total_steps"] += 1
product_orders = orders.get(product, [])
s["orders_submitted"] += len(product_orders)
new_trades = []
for order in product_orders:
qty_before = abs(order.quantity)
if order.quantity > 0:
fills = _tagged_match_buy(state, data, order, market_trades.get(product, []), mode)
elif order.quantity < 0:
fills = _tagged_match_sell(state, data, order, market_trades.get(product, []), mode)
else:
fills = []
new_trades.extend(fills)
# Track unfilled orders (order had leftover quantity)
qty_after = abs(order.quantity)
if qty_after > 0:
s["orders_unfilled"] += 1
if new_trades:
state.own_trades[product] = new_trades
result.trades.extend([TradeRow(trade) for trade in new_trades])
s["steps_with_fills"] += 1
else:
state.own_trades[product] = []
# Remaining market trades
for product, trades in market_trades.items():
for trade in trades:
trade.trade.quantity = min(trade.buy_quantity, trade.sell_quantity)
remaining = [t.trade for t in trades if t.trade.quantity > 0]
state.market_trades[product] = remaining
result.trades.extend([TradeRow(trade) for trade in remaining])
# ── Install patches ────────────────────────────────────────────────
runner.match_buy_order = _tagged_match_buy
runner.match_sell_order = _tagged_match_sell
runner.match_orders = _tagged_match_orders
# ── Report printer ─────────────────────────────────────────────────
def print_fill_report():
if not stats:
print("\nNo fills recorded.")
return
print("\n" + "=" * 72)
print(" FILL ANALYTICS")
print("=" * 72)
for product in sorted(stats):
s = stats[product]
total_steps = s["total_steps"]
maker_fills = s["maker_buys"] + s["maker_sells"]
taker_fills = s["taker_buys"] + s["taker_sells"]
total_fills = maker_fills + taker_fills
maker_vol = s["maker_buy_vol"] + s["maker_sell_vol"]
taker_vol = s["taker_buy_vol"] + s["taker_sell_vol"]
total_vol = maker_vol + taker_vol
maker_cost = s["maker_buy_cost"] + s["maker_sell_cost"]
taker_cost = s["taker_buy_cost"] + s["taker_sell_cost"]
print(f"\n {product}")
print(f" {'─' * 60}")
# Fill counts
pct_m = maker_fills / total_fills * 100 if total_fills else 0
pct_t = taker_fills / total_fills * 100 if total_fills else 0
print(f" Fills: {total_fills:>6} │ maker {maker_fills:>5} ({pct_m:5.1f}%) │ taker {taker_fills:>5} ({pct_t:5.1f}%)")
# Volume
vpct_m = maker_vol / total_vol * 100 if total_vol else 0
vpct_t = taker_vol / total_vol * 100 if total_vol else 0
print(f" Volume: {total_vol:>6} │ maker {maker_vol:>5} ({vpct_m:5.1f}%) │ taker {taker_vol:>5} ({vpct_t:5.1f}%)")
# Avg fill size
avg_maker = maker_vol / maker_fills if maker_fills else 0
avg_taker = taker_vol / taker_fills if taker_fills else 0
avg_all = total_vol / total_fills if total_fills else 0
print(f" Avg qty: {avg_all:>6.1f} │ maker {avg_maker:>5.1f} │ taker {avg_taker:>5.1f}")
# Avg price
avg_maker_px = maker_cost / maker_vol if maker_vol else 0
avg_taker_px = taker_cost / taker_vol if taker_vol else 0
avg_all_px = (maker_cost + taker_cost) / total_vol if total_vol else 0
print(f" Avg px: {avg_all_px:>8.1f}│ maker {avg_maker_px:>8.1f} │ taker {avg_taker_px:>8.1f}")
# Buy/sell breakdown
print(f" ┌─ Buys: maker {s['maker_buys']:>4} ({s['maker_buy_vol']:>5} vol) │ taker {s['taker_buys']:>4} ({s['taker_buy_vol']:>5} vol)")
print(f" └─ Sells: maker {s['maker_sells']:>4} ({s['maker_sell_vol']:>5} vol) │ taker {s['taker_sells']:>4} ({s['taker_sell_vol']:>5} vol)")
# Step-level stats
fills_per_step = total_fills / total_steps if total_steps else 0
vol_per_step = total_vol / total_steps if total_steps else 0
fill_rate = s["steps_with_fills"] / total_steps * 100 if total_steps else 0
print(f" Steps: {total_steps:>6} │ with fills {s['steps_with_fills']:>5} ({fill_rate:5.1f}%)")
print(f" Per step: {fills_per_step:>5.2f} fills │ {vol_per_step:>5.1f} vol")
# Order efficiency
submitted = s["orders_submitted"]
unfilled = s["orders_unfilled"]
fully_filled = submitted - unfilled
fill_pct = fully_filled / submitted * 100 if submitted else 0
print(f" Orders: {submitted:>6} submitted │ {fully_filled:>5} fully filled ({fill_pct:5.1f}%)")
print("\n" + "=" * 72)
# ── Main: delegate to backtester CLI, then print report ────────────
if __name__ == "__main__":
from prosperity3bt.__main__ import app
try:
app(standalone_mode=False)
except SystemExit:
pass
print_fill_report()