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.
Files changed (25) hide show
  1. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/PKG-INFO +11 -8
  2. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/README.md +5 -3
  3. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/__main__.py +58 -102
  4. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/data.py +6 -6
  5. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/models.py +7 -0
  6. prosperity3bt-0.3.0/prosperity3bt/open.py +36 -0
  7. prosperity3bt-0.3.0/prosperity3bt/parse_submission_logs.py +87 -0
  8. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/prices_round_0_day_-2.csv +1 -1
  9. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/runner.py +54 -17
  10. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/PKG-INFO +11 -8
  11. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/SOURCES.txt +2 -0
  12. prosperity3bt-0.3.0/prosperity3bt.egg-info/requires.txt +5 -0
  13. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/pyproject.toml +16 -2
  14. prosperity3bt-0.1.0/prosperity3bt.egg-info/requires.txt +0 -4
  15. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/LICENSE +0 -0
  16. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/__init__.py +0 -0
  17. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/datamodel.py +0 -0
  18. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/file_reader.py +0 -0
  19. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/__init__.py +0 -0
  20. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/__init__.py +0 -0
  21. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt/resources/round0/trades_round_0_day_-2_nn.csv +0 -0
  22. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/dependency_links.txt +0 -0
  23. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/entry_points.txt +0 -0
  24. {prosperity3bt-0.1.0 → prosperity3bt-0.3.0}/prosperity3bt.egg-info/top_level.txt +0 -0
  25. {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.1.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 partial, reduce
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: str) -> Any:
19
- algorithm_path = Path(algorithm).expanduser().resolve()
20
- if not algorithm_path.is_file():
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[str]) -> FileReader:
23
+ def parse_data(data_root: Optional[Path]) -> FileReader:
28
24
  if data_root is not None:
29
- return FileSystemReader(Path(data_root).expanduser().resolve())
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[str], no_out: bool) -> Optional[Path]:
63
+ def parse_out(out: Optional[Path], no_out: bool) -> Optional[Path]:
68
64
  if out is not None:
69
- return Path(out).expanduser().resolve()
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 main() -> None:
202
- parser = ArgumentParser(prog="prosperity3bt", description="Run a backtest.")
203
- parser.add_argument("algorithm", type=str, help="path to the Python file containing the algoritm to backtest")
204
- parser.add_argument(
205
- "days",
206
- type=str,
207
- nargs="+",
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
- if args.out is not None and args.no_out:
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(args.algorithm)
203
+ trader_module = parse_algorithm(algorithm)
252
204
  except ModuleNotFoundError as e:
253
- print(f"{args.algorithm} is not a valid algorithm file: {e}")
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"{args.algorithm} does not expose a Trader class")
209
+ print(f"{algorithm} does not expose a Trader class")
258
210
  sys.exit(1)
259
211
 
260
- file_reader = parse_data(args.data)
261
- days = parse_days(file_reader, args.days)
262
- output_file = parse_out(args.out, args.no_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 args.no_progress and not args.print
216
+ show_progress_bars = not no_progress and not print_output
265
217
 
266
218
  results = []
267
- for round_num, day_num in days:
268
- print(f"Backtesting {args.algorithm} on round {round_num} day {day_num}")
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
- args.print,
278
- args.no_trades_matching,
279
- True, # args.no_names,
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(days) > 1:
236
+ if len(parsed_days) > 1:
285
237
  print()
286
238
 
287
239
  results.append(result)
288
240
 
289
- if len(days) > 1:
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, args.merge_pnl, not args.original_timestamps), results)
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 args.vis:
298
- open_visualizer(output_file, args.vis_requests)
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, int]
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) -> Optional[BacktestData]:
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
- return None
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
- continue
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()
@@ -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;18;2022;28;;;2019.5;0.0
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 ActivityLogRow, BacktestResult, MarketTrade, SandboxLogRow, TradeRow
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, data: BacktestData, order: Order, market_trades: list[MarketTrade]
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 market_trade.sell_quantity == 0 or market_trade.trade.price > order.price:
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, data: BacktestData, order: Order, market_trades: list[MarketTrade]
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 market_trade.buy_quantity == 0 or market_trade.trade.price < order.price:
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(state: TradingState, data: BacktestData, order: Order, market_trades: list[MarketTrade]) -> list[Trade]:
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
- disable_trades_matching: bool,
236
+ trade_matching_mode: TradeMatchingMode,
202
237
  ) -> None:
203
- market_trades: dict[Symbol, list[MarketTrade]] = {}
204
- for product, trades in data.trades[state.timestamp].items():
205
- market_trades[product] = [MarketTrade(t, t.quantity, t.quantity) for t in trades]
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
- [] if disable_trades_matching else market_trades.get(product, []),
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
- disable_trades_matching: bool,
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, disable_trades_matching)
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.1.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
@@ -0,0 +1,5 @@
1
+ ipython>=8.18.1
2
+ jsonpickle>=4.0.2
3
+ orjson>=3.10.15
4
+ tqdm>=4.67.1
5
+ typer>=0.15.2
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "prosperity3bt"
3
3
  description = "Backtester for IMC Prosperity 3 algorithms"
4
- version = "0.1.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 = ["ipython", "jsonpickle", "orjson", "tqdm"]
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
@@ -1,4 +0,0 @@
1
- ipython
2
- jsonpickle
3
- orjson
4
- tqdm
File without changes
File without changes