prosperity3bt 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/PKG-INFO +11 -8
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/README.md +5 -3
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/__main__.py +58 -102
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/data.py +6 -6
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/models.py +7 -0
- prosperity3bt-0.3.0/prosperity3bt/open.py +36 -0
- prosperity3bt-0.3.0/prosperity3bt/parse_submission_logs.py +87 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/prices_round_0_day_-2.csv +1 -1
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/runner.py +54 -17
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/PKG-INFO +11 -8
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/SOURCES.txt +2 -0
- prosperity3bt-0.3.0/prosperity3bt.egg-info/requires.txt +5 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/pyproject.toml +16 -2
- prosperity3bt-0.1.0/prosperity3bt.egg-info/requires.txt +0 -4
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/LICENSE +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/__init__.py +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/datamodel.py +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/file_reader.py +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/__init__.py +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/__init__.py +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/trades_round_0_day_-2_nn.csv +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/dependency_links.txt +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/entry_points.txt +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/top_level.txt +0 -0
- {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: prosperity3bt
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Backtester for IMC Prosperity 3 algorithms
|
5
5
|
Author-email: Jasper van Merle <jaspervmerle@gmail.com>
|
6
6
|
License: MIT License
|
@@ -37,10 +37,11 @@ Classifier: Programming Language :: Python :: 3
|
|
37
37
|
Requires-Python: >=3.9
|
38
38
|
Description-Content-Type: text/markdown
|
39
39
|
License-File: LICENSE
|
40
|
-
Requires-Dist: ipython
|
41
|
-
Requires-Dist: jsonpickle
|
42
|
-
Requires-Dist: orjson
|
43
|
-
Requires-Dist: tqdm
|
40
|
+
Requires-Dist: ipython>=8.18.1
|
41
|
+
Requires-Dist: jsonpickle>=4.0.2
|
42
|
+
Requires-Dist: orjson>=3.10.15
|
43
|
+
Requires-Dist: tqdm>=4.67.1
|
44
|
+
Requires-Dist: typer>=0.15.2
|
44
45
|
|
45
46
|
# IMC Prosperity 3 Backtester
|
46
47
|
|
@@ -98,15 +99,17 @@ $ prosperity3bt example/starter.py 1 --data prosperity3bt/resources
|
|
98
99
|
# Print trader's output to stdout while running
|
99
100
|
# This may be helpful when debugging a broken trader
|
100
101
|
$ prosperity3bt example/starter.py 1 --print
|
101
|
-
|
102
|
-
# Only match orders against order depths, not against market trades
|
103
|
-
$ prosperity3bt example/starter.py 1 --no-trades-matching
|
104
102
|
```
|
105
103
|
|
106
104
|
## Order Matching
|
107
105
|
|
108
106
|
Orders placed by `Trader.run` at a given timestamp are matched against the order depths and market trades of that timestamp's state. Order depths take priority, if an order can be filled completely using volume in the relevant order depth, market trades are not considered. If not, the backtester matches your order against the timestamp's market trades. In this case the backtester assumes that for each trade, the buyer and the seller of the trade are willing to trade with you instead at the trade's price and volume. Market trades are matched at the price of your orders, e.g. if you place a sell order for €9 and there is a market trade for €10, the sell order is matched at €9 (even though there is a buyer willing to pay €10, this appears to be consistent with what the official Prosperity environment does).
|
109
107
|
|
108
|
+
Matching orders against market trades can be configured through the `--match-trades` option:
|
109
|
+
- `--match-trades all` (default): match market trades with prices equal to or worse than your quotes.
|
110
|
+
- `--match-trades worse`: match market trades with prices worse than your quotes, inspired by [team Linear Utility's Prosperity 2 write-up](https://github.com/ericcccsliu/imc-prosperity-2).
|
111
|
+
- `--match-trades none`: do not match market trades against orders.
|
112
|
+
|
110
113
|
Limits are enforced before orders are matched to order depths. If for a product your position would exceed the limit, assuming all your orders would get filled, all your orders for that product get canceled.
|
111
114
|
|
112
115
|
## Data Files
|
@@ -54,15 +54,17 @@ $ prosperity3bt example/starter.py 1 --data prosperity3bt/resources
|
|
54
54
|
# Print trader's output to stdout while running
|
55
55
|
# This may be helpful when debugging a broken trader
|
56
56
|
$ prosperity3bt example/starter.py 1 --print
|
57
|
-
|
58
|
-
# Only match orders against order depths, not against market trades
|
59
|
-
$ prosperity3bt example/starter.py 1 --no-trades-matching
|
60
57
|
```
|
61
58
|
|
62
59
|
## Order Matching
|
63
60
|
|
64
61
|
Orders placed by `Trader.run` at a given timestamp are matched against the order depths and market trades of that timestamp's state. Order depths take priority, if an order can be filled completely using volume in the relevant order depth, market trades are not considered. If not, the backtester matches your order against the timestamp's market trades. In this case the backtester assumes that for each trade, the buyer and the seller of the trade are willing to trade with you instead at the trade's price and volume. Market trades are matched at the price of your orders, e.g. if you place a sell order for €9 and there is a market trade for €10, the sell order is matched at €9 (even though there is a buyer willing to pay €10, this appears to be consistent with what the official Prosperity environment does).
|
65
62
|
|
63
|
+
Matching orders against market trades can be configured through the `--match-trades` option:
|
64
|
+
- `--match-trades all` (default): match market trades with prices equal to or worse than your quotes.
|
65
|
+
- `--match-trades worse`: match market trades with prices worse than your quotes, inspired by [team Linear Utility's Prosperity 2 write-up](https://github.com/ericcccsliu/imc-prosperity-2).
|
66
|
+
- `--match-trades none`: do not match market trades against orders.
|
67
|
+
|
66
68
|
Limits are enforced before orders are matched to order depths. If for a product your position would exceed the limit, assuming all your orders would get filled, all your orders for that product get canceled.
|
67
69
|
|
68
70
|
## Data Files
|
@@ -1,32 +1,28 @@
|
|
1
1
|
import sys
|
2
|
-
import webbrowser
|
3
|
-
from argparse import ArgumentParser
|
4
2
|
from collections import defaultdict
|
5
3
|
from datetime import datetime
|
6
|
-
from functools import
|
7
|
-
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
4
|
+
from functools import reduce
|
8
5
|
from importlib import import_module, metadata, reload
|
9
6
|
from pathlib import Path
|
10
|
-
from typing import Any, Optional
|
7
|
+
from typing import Annotated, Any, Optional
|
8
|
+
|
9
|
+
from typer import Argument, Exit, Option, Typer
|
11
10
|
|
12
11
|
from prosperity3bt.data import has_day_data
|
13
12
|
from prosperity3bt.file_reader import FileReader, FileSystemReader, PackageResourcesReader
|
14
|
-
from prosperity3bt.models import BacktestResult
|
13
|
+
from prosperity3bt.models import BacktestResult, TradeMatchingMode
|
14
|
+
from prosperity3bt.open import open_visualizer
|
15
15
|
from prosperity3bt.runner import run_backtest
|
16
16
|
|
17
17
|
|
18
|
-
def parse_algorithm(algorithm:
|
19
|
-
|
20
|
-
|
21
|
-
raise ModuleNotFoundError(f"{algorithm_path} is not a file")
|
22
|
-
|
23
|
-
sys.path.append(str(algorithm_path.parent))
|
24
|
-
return import_module(algorithm_path.stem)
|
18
|
+
def parse_algorithm(algorithm: Path) -> Any:
|
19
|
+
sys.path.append(str(algorithm.parent))
|
20
|
+
return import_module(algorithm.stem)
|
25
21
|
|
26
22
|
|
27
|
-
def parse_data(data_root: Optional[
|
23
|
+
def parse_data(data_root: Optional[Path]) -> FileReader:
|
28
24
|
if data_root is not None:
|
29
|
-
return FileSystemReader(
|
25
|
+
return FileSystemReader(data_root)
|
30
26
|
else:
|
31
27
|
return PackageResourcesReader()
|
32
28
|
|
@@ -64,9 +60,9 @@ def parse_days(file_reader: FileReader, days: list[str]) -> list[tuple[int, int]
|
|
64
60
|
return parsed_days
|
65
61
|
|
66
62
|
|
67
|
-
def parse_out(out: Optional[
|
63
|
+
def parse_out(out: Optional[Path], no_out: bool) -> Optional[Path]:
|
68
64
|
if out is not None:
|
69
|
-
return
|
65
|
+
return out
|
70
66
|
|
71
67
|
if no_out:
|
72
68
|
return None
|
@@ -167,29 +163,6 @@ def print_overall_summary(results: list[BacktestResult]) -> None:
|
|
167
163
|
print(f"Total profit: {total_profit:,.0f}")
|
168
164
|
|
169
165
|
|
170
|
-
class HTTPRequestHandler(SimpleHTTPRequestHandler):
|
171
|
-
def end_headers(self) -> None:
|
172
|
-
self.send_header("Access-Control-Allow-Origin", "*")
|
173
|
-
return super().end_headers()
|
174
|
-
|
175
|
-
def log_message(self, format: str, *args: Any) -> None:
|
176
|
-
return
|
177
|
-
|
178
|
-
|
179
|
-
def open_visualizer(output_file: Path, no_requests: int) -> None:
|
180
|
-
http_handler = partial(HTTPRequestHandler, directory=output_file.parent)
|
181
|
-
http_server = HTTPServer(("localhost", 0), http_handler)
|
182
|
-
|
183
|
-
webbrowser.open(
|
184
|
-
f"https://jmerle.github.io/imc-prosperity-3-visualizer/?open=http://localhost:{http_server.server_port}/{output_file.name}"
|
185
|
-
)
|
186
|
-
|
187
|
-
# Chrome makes 2 requests: 1 OPTIONS request to check for CORS headers and 1 GET request to get the data
|
188
|
-
# Some users reported their browser only makes 1 request, which is covered by the --vis-requests option
|
189
|
-
for _ in range(no_requests):
|
190
|
-
http_server.handle_request()
|
191
|
-
|
192
|
-
|
193
166
|
def format_path(path: Path) -> str:
|
194
167
|
cwd = Path.cwd()
|
195
168
|
if path.is_relative_to(cwd):
|
@@ -198,74 +171,53 @@ def format_path(path: Path) -> str:
|
|
198
171
|
return str(path)
|
199
172
|
|
200
173
|
|
201
|
-
def
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
help="the days to backtest on (<round>-<day> for a single day, <round> for all days in a round)",
|
209
|
-
)
|
210
|
-
parser.add_argument("--merge-pnl", action="store_true", help="merge profit and loss across days")
|
211
|
-
parser.add_argument("--vis", action="store_true", help="open backtest result in visualizer when done")
|
212
|
-
parser.add_argument("--out", type=str, help="path to save output log to (defaults to backtests/<timestamp>.log)")
|
213
|
-
parser.add_argument(
|
214
|
-
"--data",
|
215
|
-
type=str,
|
216
|
-
help="path to data directory (must look similar in structure to https://github.com/jmerle/imc-prosperity-3-backtester/tree/master/prosperity3bt/resources)",
|
217
|
-
)
|
218
|
-
parser.add_argument("--print", action="store_true", help="print the trader's output to stdout while it's running")
|
219
|
-
parser.add_argument(
|
220
|
-
"--no-trades-matching", action="store_true", help="disable matching orders against market trades"
|
221
|
-
)
|
222
|
-
parser.add_argument("--no-out", action="store_true", help="skip saving the output log to a file")
|
223
|
-
parser.add_argument("--no-progress", action="store_true", help="don't show progress bars")
|
224
|
-
parser.add_argument(
|
225
|
-
"--vis-requests",
|
226
|
-
type=int,
|
227
|
-
default=2,
|
228
|
-
help="number of requests the visualizer is expected to make to the backtester's HTTP server when using --vis",
|
229
|
-
)
|
230
|
-
parser.add_argument(
|
231
|
-
"--original-timestamps",
|
232
|
-
action="store_true",
|
233
|
-
help="preserve original timestamps in output log rather than making them increase across days",
|
234
|
-
)
|
235
|
-
# parser.add_argument(
|
236
|
-
# "--no-names", action="store_true", help="don't use de-anonymized trades data, even if it exists"
|
237
|
-
# )
|
238
|
-
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {metadata.version(__package__)}")
|
239
|
-
|
240
|
-
args = parser.parse_args()
|
241
|
-
|
242
|
-
if args.vis and args.no_out:
|
243
|
-
print("Error: --vis and --no-out are mutually exclusive")
|
244
|
-
sys.exit(1)
|
174
|
+
def version_callback(value: bool) -> None:
|
175
|
+
if value:
|
176
|
+
print(f"prosperity3bt {metadata.version(__package__)}")
|
177
|
+
raise Exit()
|
178
|
+
|
179
|
+
|
180
|
+
app = Typer(context_settings={"help_option_names": ["--help", "-h"]})
|
245
181
|
|
246
|
-
|
182
|
+
|
183
|
+
@app.command()
|
184
|
+
def cli(
|
185
|
+
algorithm: Annotated[Path, Argument(help="Path to the Python file containing the algorithm to backtest.", show_default=False, exists=True, file_okay=True, dir_okay=False, resolve_path=True)],
|
186
|
+
days: Annotated[list[str], Argument(help="The days to backtest on. <round>-<day> for a single day, <round> for all days in a round.", show_default=False)],
|
187
|
+
merge_pnl: Annotated[bool, Option("--merge-pnl", help="Merge profit and loss across days.")] = False,
|
188
|
+
vis: Annotated[bool, Option("--vis", help="Open backtest results in https://jmerle.github.io/imc-prosperity-3-visualizer/ when done.")] = False,
|
189
|
+
out: Annotated[Optional[Path], Option(help="Path to save output log to (defaults to backtests/<timestamp>.log).", show_default=False, dir_okay=False, resolve_path=True)] = None,
|
190
|
+
data: Annotated[Optional[Path], Option(help="Path to data directory. Must look similar in structure to https://github.com/jmerle/imc-prosperity-3-backtester/tree/master/prosperity3bt/resources.", show_default=False, exists=True, file_okay=False, dir_okay=True, resolve_path=True)] = None,
|
191
|
+
print_output: Annotated[bool, Option("--print", help="Print the trader's output to stdout while it's running.")] = False,
|
192
|
+
match_trades: Annotated[TradeMatchingMode, Option(help="How to match orders against market trades. 'all' matches trades with prices equal to or worse than your quotes, 'worse' matches trades with prices worse than your quotes, 'none' does not match trades against orders at all.")] = TradeMatchingMode.all,
|
193
|
+
no_out: Annotated[bool, Option("--no-out", help="Skip saving the output log to a file.")] = False,
|
194
|
+
no_progress: Annotated[bool, Option("--no-progress", help="Don't show progress bars.")] = False,
|
195
|
+
original_timestamps: Annotated[bool, Option("--original-timestamps", help="Preserve original timestamps in output log rather than making them increase across days.")] = False,
|
196
|
+
version: Annotated[bool, Option("--version", "-v", help="Show the program's version number and exit.", is_eager=True, callback=version_callback)] = False,
|
197
|
+
) -> None: # fmt: skip
|
198
|
+
if out is not None and no_out:
|
247
199
|
print("Error: --out and --no-out are mutually exclusive")
|
248
200
|
sys.exit(1)
|
249
201
|
|
250
202
|
try:
|
251
|
-
trader_module = parse_algorithm(
|
203
|
+
trader_module = parse_algorithm(algorithm)
|
252
204
|
except ModuleNotFoundError as e:
|
253
|
-
print(f"{
|
205
|
+
print(f"{algorithm} is not a valid algorithm file: {e}")
|
254
206
|
sys.exit(1)
|
255
207
|
|
256
208
|
if not hasattr(trader_module, "Trader"):
|
257
|
-
print(f"{
|
209
|
+
print(f"{algorithm} does not expose a Trader class")
|
258
210
|
sys.exit(1)
|
259
211
|
|
260
|
-
file_reader = parse_data(
|
261
|
-
|
262
|
-
output_file = parse_out(
|
212
|
+
file_reader = parse_data(data)
|
213
|
+
parsed_days = parse_days(file_reader, days)
|
214
|
+
output_file = parse_out(out, no_out)
|
263
215
|
|
264
|
-
show_progress_bars = not
|
216
|
+
show_progress_bars = not no_progress and not print_output
|
265
217
|
|
266
218
|
results = []
|
267
|
-
for round_num, day_num in
|
268
|
-
print(f"Backtesting {
|
219
|
+
for round_num, day_num in parsed_days:
|
220
|
+
print(f"Backtesting {algorithm} on round {round_num} day {day_num}")
|
269
221
|
|
270
222
|
reload(trader_module)
|
271
223
|
|
@@ -274,28 +226,32 @@ def main() -> None:
|
|
274
226
|
file_reader,
|
275
227
|
round_num,
|
276
228
|
day_num,
|
277
|
-
|
278
|
-
|
279
|
-
True,
|
229
|
+
print_output,
|
230
|
+
match_trades,
|
231
|
+
True,
|
280
232
|
show_progress_bars,
|
281
233
|
)
|
282
234
|
|
283
235
|
print_day_summary(result)
|
284
|
-
if len(
|
236
|
+
if len(parsed_days) > 1:
|
285
237
|
print()
|
286
238
|
|
287
239
|
results.append(result)
|
288
240
|
|
289
|
-
if len(
|
241
|
+
if len(parsed_days) > 1:
|
290
242
|
print_overall_summary(results)
|
291
243
|
|
292
244
|
if output_file is not None:
|
293
|
-
merged_results = reduce(lambda a, b: merge_results(a, b,
|
245
|
+
merged_results = reduce(lambda a, b: merge_results(a, b, merge_pnl, not original_timestamps), results)
|
294
246
|
write_output(output_file, merged_results)
|
295
247
|
print(f"\nSuccessfully saved backtest results to {format_path(output_file)}")
|
296
248
|
|
297
|
-
if
|
298
|
-
open_visualizer(output_file
|
249
|
+
if vis and output_file is not None:
|
250
|
+
open_visualizer(output_file)
|
251
|
+
|
252
|
+
|
253
|
+
def main() -> None:
|
254
|
+
app()
|
299
255
|
|
300
256
|
|
301
257
|
if __name__ == "__main__":
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from collections import defaultdict
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import Optional
|
4
3
|
|
5
4
|
from prosperity3bt.datamodel import Symbol, Trade
|
6
5
|
from prosperity3bt.file_reader import FileReader
|
@@ -45,7 +44,7 @@ class BacktestData:
|
|
45
44
|
prices: dict[int, dict[Symbol, PriceRow]]
|
46
45
|
trades: dict[int, dict[Symbol, list[Trade]]]
|
47
46
|
products: list[Symbol]
|
48
|
-
profit_loss: dict[Symbol,
|
47
|
+
profit_loss: dict[Symbol, float]
|
49
48
|
|
50
49
|
|
51
50
|
def create_backtest_data(round_num: int, day_num: int, prices: list[PriceRow], trades: list[Trade]) -> BacktestData:
|
@@ -58,7 +57,7 @@ def create_backtest_data(round_num: int, day_num: int, prices: list[PriceRow], t
|
|
58
57
|
trades_by_timestamp[trade.timestamp][trade.symbol].append(trade)
|
59
58
|
|
60
59
|
products = sorted(set(row.product for row in prices))
|
61
|
-
profit_loss = {product: 0 for product in products}
|
60
|
+
profit_loss = {product: 0.0 for product in products}
|
62
61
|
|
63
62
|
return BacktestData(
|
64
63
|
round_num=round_num,
|
@@ -75,11 +74,11 @@ def has_day_data(file_reader: FileReader, round_num: int, day_num: int) -> bool:
|
|
75
74
|
return file is not None
|
76
75
|
|
77
76
|
|
78
|
-
def read_day_data(file_reader: FileReader, round_num: int, day_num: int, no_names: bool) ->
|
77
|
+
def read_day_data(file_reader: FileReader, round_num: int, day_num: int, no_names: bool) -> BacktestData:
|
79
78
|
prices = []
|
80
79
|
with file_reader.file([f"round{round_num}", f"prices_round_{round_num}_day_{day_num}.csv"]) as file:
|
81
80
|
if file is None:
|
82
|
-
|
81
|
+
raise ValueError(f"Prices data is not available for round {round_num} day {day_num}")
|
83
82
|
|
84
83
|
for line in file.read_text(encoding="utf-8").splitlines()[1:]:
|
85
84
|
columns = line.split(";")
|
@@ -104,7 +103,8 @@ def read_day_data(file_reader: FileReader, round_num: int, day_num: int, no_name
|
|
104
103
|
for suffix in trades_suffixes:
|
105
104
|
with file_reader.file([f"round{round_num}", f"trades_round_{round_num}_day_{day_num}_{suffix}.csv"]) as file:
|
106
105
|
if file is None:
|
107
|
-
|
106
|
+
trades_data_type = "Anonymized" if suffix == "nn" else "De-anonymized"
|
107
|
+
raise ValueError(f"{trades_data_type} trades data is not available for round {round_num} day {day_num}")
|
108
108
|
|
109
109
|
for line in file.read_text(encoding="utf-8").splitlines()[1:]:
|
110
110
|
columns = line.split(";")
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
2
3
|
from typing import Any
|
3
4
|
|
4
5
|
import orjson
|
@@ -101,3 +102,9 @@ class MarketTrade:
|
|
101
102
|
trade: Trade
|
102
103
|
buy_quantity: int
|
103
104
|
sell_quantity: int
|
105
|
+
|
106
|
+
|
107
|
+
class TradeMatchingMode(str, Enum):
|
108
|
+
all = "all"
|
109
|
+
worse = "worse"
|
110
|
+
none = "none"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import webbrowser
|
2
|
+
from functools import partial
|
3
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
|
8
|
+
class HTTPRequestHandler(SimpleHTTPRequestHandler):
|
9
|
+
def do_GET(self):
|
10
|
+
self.server.shutdown_flag = True
|
11
|
+
return super().do_GET()
|
12
|
+
|
13
|
+
def end_headers(self) -> None:
|
14
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
15
|
+
return super().end_headers()
|
16
|
+
|
17
|
+
def log_message(self, format: str, *args: Any) -> None:
|
18
|
+
return
|
19
|
+
|
20
|
+
|
21
|
+
class CustomHTTPServer(HTTPServer):
|
22
|
+
def __init__(self, *args, **kwargs) -> None:
|
23
|
+
super().__init__(*args, **kwargs)
|
24
|
+
self.shutdown_flag = False
|
25
|
+
|
26
|
+
|
27
|
+
def open_visualizer(output_file: Path) -> None:
|
28
|
+
http_handler = partial(HTTPRequestHandler, directory=str(output_file.parent))
|
29
|
+
http_server = CustomHTTPServer(("localhost", 0), http_handler)
|
30
|
+
|
31
|
+
webbrowser.open(
|
32
|
+
f"https://jmerle.github.io/imc-prosperity-3-visualizer/?open=http://localhost:{http_server.server_port}/{output_file.name}"
|
33
|
+
)
|
34
|
+
|
35
|
+
while not http_server.shutdown_flag:
|
36
|
+
http_server.handle_request()
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import sys
|
2
|
+
from argparse import ArgumentParser
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
import orjson
|
6
|
+
|
7
|
+
|
8
|
+
def parse_prices(activities_log: str, output_dir: Path, round_day: str) -> None:
|
9
|
+
output_file = output_dir / f"prices_{round_day}.csv"
|
10
|
+
|
11
|
+
print(f"Writing prices data to {output_file}")
|
12
|
+
with output_file.open("w+", encoding="utf-8") as f:
|
13
|
+
f.write(activities_log + "\n")
|
14
|
+
|
15
|
+
|
16
|
+
def parse_trades(trade_history: str, output_dir: Path, round_day: str) -> None:
|
17
|
+
trades = orjson.loads(trade_history)
|
18
|
+
|
19
|
+
with_names_options = [False]
|
20
|
+
if len(trades) > 0 and len(trades[0]["buyer"]) > 0 and len(trades[0]["seller"]) > 0:
|
21
|
+
with_names_options.append(True)
|
22
|
+
|
23
|
+
for with_names in with_names_options:
|
24
|
+
suffix = "wn" if with_names else "nn"
|
25
|
+
output_file = output_dir / f"trades_{round_day}_{suffix}.csv"
|
26
|
+
|
27
|
+
print(f"Writing trades data to {output_file}")
|
28
|
+
with output_file.open("w+", encoding="utf-8") as f:
|
29
|
+
f.write("timestamp;buyer;seller;symbol;currency;price;quantity\n")
|
30
|
+
|
31
|
+
for t in trades:
|
32
|
+
row = ";".join(
|
33
|
+
[
|
34
|
+
str(t["timestamp"]),
|
35
|
+
t["buyer"] if with_names else "",
|
36
|
+
t["seller"] if with_names else "",
|
37
|
+
t["symbol"],
|
38
|
+
t["currency"],
|
39
|
+
str(t["price"]),
|
40
|
+
str(t["quantity"]),
|
41
|
+
]
|
42
|
+
)
|
43
|
+
|
44
|
+
f.write(row + "\n")
|
45
|
+
|
46
|
+
|
47
|
+
def main() -> None:
|
48
|
+
parser = ArgumentParser(
|
49
|
+
description="Save prices and trades data in submission logs to prosperity3bt's resources module.",
|
50
|
+
)
|
51
|
+
parser.add_argument("file", type=str, help="path to the log file")
|
52
|
+
parser.add_argument("round", type=int, help="round the logs belong to")
|
53
|
+
parser.add_argument("day", type=int, help="day the logs belong to")
|
54
|
+
|
55
|
+
args = parser.parse_args()
|
56
|
+
|
57
|
+
file = Path(args.file).expanduser().resolve()
|
58
|
+
if not file.is_file():
|
59
|
+
print(f"Error: {file} is not a file")
|
60
|
+
sys.exit(1)
|
61
|
+
|
62
|
+
logs = file.read_text()
|
63
|
+
|
64
|
+
sections = {}
|
65
|
+
for block in logs.split("\n\n"):
|
66
|
+
block = block.strip()
|
67
|
+
if len(block) == 0:
|
68
|
+
continue
|
69
|
+
|
70
|
+
newline_idx = block.index("\n")
|
71
|
+
category = block[: newline_idx - 1]
|
72
|
+
content = block[newline_idx + 1 :]
|
73
|
+
|
74
|
+
sections[category] = content
|
75
|
+
|
76
|
+
output_dir = Path(__file__).parent / "resources" / f"round{args.round}"
|
77
|
+
if not output_dir.is_dir():
|
78
|
+
output_dir.mkdir(parents=True)
|
79
|
+
|
80
|
+
round_day = f"round_{args.round}_day_{args.day}"
|
81
|
+
|
82
|
+
parse_prices(sections["Activities log"], output_dir, round_day)
|
83
|
+
parse_trades(sections["Trade History"], output_dir, round_day)
|
84
|
+
|
85
|
+
|
86
|
+
if __name__ == "__main__":
|
87
|
+
main()
|
{prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/prices_round_0_day_-2.csv
RENAMED
@@ -2070,7 +2070,7 @@ day;timestamp;product;bid_price_1;bid_volume_1;bid_price_2;bid_volume_2;bid_pric
|
|
2070
2070
|
-1;103400;KELP;2018;32;;;;;2019;1;2020;5;2022;32;2018.5;0.0
|
2071
2071
|
-1;103400;RAINFOREST_RESIN;9998;9;9996;2;9995;30;10000;4;10004;2;10005;30;9999.0;0.0
|
2072
2072
|
-1;103500;RAINFOREST_RESIN;9996;1;9995;27;;;10004;1;10005;27;;;10000.0;0.0
|
2073
|
-
-1;103500;KELP;2019;8;2018;28;;;2020;
|
2073
|
+
-1;103500;KELP;2019;8;2018;28;;;2020;9;2022;28;;;2019.5;0.0
|
2074
2074
|
-1;103600;RAINFOREST_RESIN;9996;2;9995;25;;;10004;2;10005;25;;;10000.0;0.0
|
2075
2075
|
-1;103600;KELP;2018;27;;;;;2022;27;;;;;2020.0;0.0
|
2076
2076
|
-1;103700;KELP;2019;1;2018;24;;;2022;25;;;;;2020.5;0.0
|
@@ -9,7 +9,14 @@ from tqdm import tqdm
|
|
9
9
|
from prosperity3bt.data import LIMITS, BacktestData, read_day_data
|
10
10
|
from prosperity3bt.datamodel import Observation, Order, OrderDepth, Symbol, Trade, TradingState
|
11
11
|
from prosperity3bt.file_reader import FileReader
|
12
|
-
from prosperity3bt.models import
|
12
|
+
from prosperity3bt.models import (
|
13
|
+
ActivityLogRow,
|
14
|
+
BacktestResult,
|
15
|
+
MarketTrade,
|
16
|
+
SandboxLogRow,
|
17
|
+
TradeMatchingMode,
|
18
|
+
TradeRow,
|
19
|
+
)
|
13
20
|
|
14
21
|
|
15
22
|
def prepare_state(state: TradingState, data: BacktestData) -> None:
|
@@ -25,7 +32,7 @@ def prepare_state(state: TradingState, data: BacktestData) -> None:
|
|
25
32
|
|
26
33
|
state.order_depths[product] = order_depth
|
27
34
|
|
28
|
-
state.listings[product] = {
|
35
|
+
state.listings[product] = { # type: ignore[assignment]
|
29
36
|
"symbol": product,
|
30
37
|
"product": product,
|
31
38
|
"denomination": 1,
|
@@ -97,7 +104,11 @@ def enforce_limits(
|
|
97
104
|
|
98
105
|
|
99
106
|
def match_buy_order(
|
100
|
-
state: TradingState,
|
107
|
+
state: TradingState,
|
108
|
+
data: BacktestData,
|
109
|
+
order: Order,
|
110
|
+
market_trades: list[MarketTrade],
|
111
|
+
trade_matching_mode: TradeMatchingMode,
|
101
112
|
) -> list[Trade]:
|
102
113
|
trades = []
|
103
114
|
|
@@ -119,8 +130,15 @@ def match_buy_order(
|
|
119
130
|
if order.quantity == 0:
|
120
131
|
return trades
|
121
132
|
|
133
|
+
if trade_matching_mode == TradeMatchingMode.none:
|
134
|
+
return trades
|
135
|
+
|
122
136
|
for market_trade in market_trades:
|
123
|
-
if
|
137
|
+
if (
|
138
|
+
market_trade.sell_quantity == 0
|
139
|
+
or market_trade.trade.price > order.price
|
140
|
+
or (market_trade.trade.price == order.price and trade_matching_mode == TradeMatchingMode.worse)
|
141
|
+
):
|
124
142
|
continue
|
125
143
|
|
126
144
|
volume = min(order.quantity, market_trade.sell_quantity)
|
@@ -142,7 +160,11 @@ def match_buy_order(
|
|
142
160
|
|
143
161
|
|
144
162
|
def match_sell_order(
|
145
|
-
state: TradingState,
|
163
|
+
state: TradingState,
|
164
|
+
data: BacktestData,
|
165
|
+
order: Order,
|
166
|
+
market_trades: list[MarketTrade],
|
167
|
+
trade_matching_mode: TradeMatchingMode,
|
146
168
|
) -> list[Trade]:
|
147
169
|
trades = []
|
148
170
|
|
@@ -164,8 +186,15 @@ def match_sell_order(
|
|
164
186
|
if order.quantity == 0:
|
165
187
|
return trades
|
166
188
|
|
189
|
+
if trade_matching_mode == TradeMatchingMode.none:
|
190
|
+
return trades
|
191
|
+
|
167
192
|
for market_trade in market_trades:
|
168
|
-
if
|
193
|
+
if (
|
194
|
+
market_trade.buy_quantity == 0
|
195
|
+
or market_trade.trade.price < order.price
|
196
|
+
or (market_trade.trade.price == order.price and trade_matching_mode == TradeMatchingMode.worse)
|
197
|
+
):
|
169
198
|
continue
|
170
199
|
|
171
200
|
volume = min(abs(order.quantity), market_trade.buy_quantity)
|
@@ -184,11 +213,17 @@ def match_sell_order(
|
|
184
213
|
return trades
|
185
214
|
|
186
215
|
|
187
|
-
def match_order(
|
216
|
+
def match_order(
|
217
|
+
state: TradingState,
|
218
|
+
data: BacktestData,
|
219
|
+
order: Order,
|
220
|
+
market_trades: list[MarketTrade],
|
221
|
+
trade_matching_mode: TradeMatchingMode,
|
222
|
+
) -> list[Trade]:
|
188
223
|
if order.quantity > 0:
|
189
|
-
return match_buy_order(state, data, order, market_trades)
|
224
|
+
return match_buy_order(state, data, order, market_trades, trade_matching_mode)
|
190
225
|
elif order.quantity < 0:
|
191
|
-
return match_sell_order(state, data, order, market_trades)
|
226
|
+
return match_sell_order(state, data, order, market_trades, trade_matching_mode)
|
192
227
|
else:
|
193
228
|
return []
|
194
229
|
|
@@ -198,11 +233,12 @@ def match_orders(
|
|
198
233
|
data: BacktestData,
|
199
234
|
orders: dict[Symbol, list[Order]],
|
200
235
|
result: BacktestResult,
|
201
|
-
|
236
|
+
trade_matching_mode: TradeMatchingMode,
|
202
237
|
) -> None:
|
203
|
-
market_trades
|
204
|
-
|
205
|
-
|
238
|
+
market_trades = {
|
239
|
+
product: [MarketTrade(t, t.quantity, t.quantity) for t in trades]
|
240
|
+
for product, trades in data.trades[state.timestamp].items()
|
241
|
+
}
|
206
242
|
|
207
243
|
for product in data.products:
|
208
244
|
new_trades = []
|
@@ -213,7 +249,8 @@ def match_orders(
|
|
213
249
|
state,
|
214
250
|
data,
|
215
251
|
order,
|
216
|
-
|
252
|
+
market_trades.get(product, []),
|
253
|
+
trade_matching_mode,
|
217
254
|
)
|
218
255
|
)
|
219
256
|
|
@@ -237,7 +274,7 @@ def run_backtest(
|
|
237
274
|
round_num: int,
|
238
275
|
day_num: int,
|
239
276
|
print_output: bool,
|
240
|
-
|
277
|
+
trade_matching_mode: TradeMatchingMode,
|
241
278
|
no_names: bool,
|
242
279
|
show_progress_bar: bool,
|
243
280
|
) -> BacktestResult:
|
@@ -279,7 +316,7 @@ def run_backtest(
|
|
279
316
|
|
280
317
|
# Tee calls stdout.close(), making stdout.getvalue() impossible
|
281
318
|
# This override makes getvalue() possible after close()
|
282
|
-
stdout.close = lambda: None
|
319
|
+
stdout.close = lambda: None # type: ignore[method-assign]
|
283
320
|
|
284
321
|
if print_output:
|
285
322
|
with closing(Tee(stdout)):
|
@@ -298,6 +335,6 @@ def run_backtest(
|
|
298
335
|
|
299
336
|
create_activity_logs(state, data, result)
|
300
337
|
enforce_limits(state, data, orders, sandbox_row)
|
301
|
-
match_orders(state, data, orders, result,
|
338
|
+
match_orders(state, data, orders, result, trade_matching_mode)
|
302
339
|
|
303
340
|
return result
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: prosperity3bt
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Backtester for IMC Prosperity 3 algorithms
|
5
5
|
Author-email: Jasper van Merle <jaspervmerle@gmail.com>
|
6
6
|
License: MIT License
|
@@ -37,10 +37,11 @@ Classifier: Programming Language :: Python :: 3
|
|
37
37
|
Requires-Python: >=3.9
|
38
38
|
Description-Content-Type: text/markdown
|
39
39
|
License-File: LICENSE
|
40
|
-
Requires-Dist: ipython
|
41
|
-
Requires-Dist: jsonpickle
|
42
|
-
Requires-Dist: orjson
|
43
|
-
Requires-Dist: tqdm
|
40
|
+
Requires-Dist: ipython>=8.18.1
|
41
|
+
Requires-Dist: jsonpickle>=4.0.2
|
42
|
+
Requires-Dist: orjson>=3.10.15
|
43
|
+
Requires-Dist: tqdm>=4.67.1
|
44
|
+
Requires-Dist: typer>=0.15.2
|
44
45
|
|
45
46
|
# IMC Prosperity 3 Backtester
|
46
47
|
|
@@ -98,15 +99,17 @@ $ prosperity3bt example/starter.py 1 --data prosperity3bt/resources
|
|
98
99
|
# Print trader's output to stdout while running
|
99
100
|
# This may be helpful when debugging a broken trader
|
100
101
|
$ prosperity3bt example/starter.py 1 --print
|
101
|
-
|
102
|
-
# Only match orders against order depths, not against market trades
|
103
|
-
$ prosperity3bt example/starter.py 1 --no-trades-matching
|
104
102
|
```
|
105
103
|
|
106
104
|
## Order Matching
|
107
105
|
|
108
106
|
Orders placed by `Trader.run` at a given timestamp are matched against the order depths and market trades of that timestamp's state. Order depths take priority, if an order can be filled completely using volume in the relevant order depth, market trades are not considered. If not, the backtester matches your order against the timestamp's market trades. In this case the backtester assumes that for each trade, the buyer and the seller of the trade are willing to trade with you instead at the trade's price and volume. Market trades are matched at the price of your orders, e.g. if you place a sell order for €9 and there is a market trade for €10, the sell order is matched at €9 (even though there is a buyer willing to pay €10, this appears to be consistent with what the official Prosperity environment does).
|
109
107
|
|
108
|
+
Matching orders against market trades can be configured through the `--match-trades` option:
|
109
|
+
- `--match-trades all` (default): match market trades with prices equal to or worse than your quotes.
|
110
|
+
- `--match-trades worse`: match market trades with prices worse than your quotes, inspired by [team Linear Utility's Prosperity 2 write-up](https://github.com/ericcccsliu/imc-prosperity-2).
|
111
|
+
- `--match-trades none`: do not match market trades against orders.
|
112
|
+
|
110
113
|
Limits are enforced before orders are matched to order depths. If for a product your position would exceed the limit, assuming all your orders would get filled, all your orders for that product get canceled.
|
111
114
|
|
112
115
|
## Data Files
|
@@ -7,6 +7,8 @@ prosperity3bt/data.py
|
|
7
7
|
prosperity3bt/datamodel.py
|
8
8
|
prosperity3bt/file_reader.py
|
9
9
|
prosperity3bt/models.py
|
10
|
+
prosperity3bt/open.py
|
11
|
+
prosperity3bt/parse_submission_logs.py
|
10
12
|
prosperity3bt/runner.py
|
11
13
|
prosperity3bt.egg-info/PKG-INFO
|
12
14
|
prosperity3bt.egg-info/SOURCES.txt
|
@@ -1,7 +1,7 @@
|
|
1
1
|
[project]
|
2
2
|
name = "prosperity3bt"
|
3
3
|
description = "Backtester for IMC Prosperity 3 algorithms"
|
4
|
-
version = "0.
|
4
|
+
version = "0.3.0"
|
5
5
|
readme = "README.md"
|
6
6
|
license = {file = "LICENSE"}
|
7
7
|
authors = [{name = "Jasper van Merle", email = "jaspervmerle@gmail.com"}]
|
@@ -14,7 +14,13 @@ classifiers = [
|
|
14
14
|
"Programming Language :: Python :: 3",
|
15
15
|
]
|
16
16
|
requires-python = ">= 3.9"
|
17
|
-
dependencies = [
|
17
|
+
dependencies = [
|
18
|
+
"ipython>=8.18.1",
|
19
|
+
"jsonpickle>=4.0.2",
|
20
|
+
"orjson>=3.10.15",
|
21
|
+
"tqdm>=4.67.1",
|
22
|
+
"typer>=0.15.2",
|
23
|
+
]
|
18
24
|
|
19
25
|
[build-system]
|
20
26
|
requires = ["setuptools >= 61.0"]
|
@@ -36,6 +42,7 @@ prosperity3bt = ["resources/*/*.csv"]
|
|
36
42
|
|
37
43
|
[tool.uv]
|
38
44
|
dev-dependencies = [
|
45
|
+
"mypy>=1.15.0",
|
39
46
|
"ruff>=0.9.7",
|
40
47
|
]
|
41
48
|
|
@@ -44,3 +51,10 @@ line-length = 120
|
|
44
51
|
|
45
52
|
[tool.ruff.lint]
|
46
53
|
extend-select = ["I"]
|
54
|
+
|
55
|
+
[tool.mypy]
|
56
|
+
ignore_missing_imports = true
|
57
|
+
|
58
|
+
[[tool.mypy.overrides]]
|
59
|
+
module = "prosperity3bt.datamodel"
|
60
|
+
ignore_errors = true
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|