cnhkmcp 2.3.2__py3-none-any.whl → 2.3.4__py3-none-any.whl

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 (56) hide show
  1. cnhkmcp/__init__.py +1 -1
  2. cnhkmcp/untracked/AI/321/206/320/231/320/243/321/205/342/225/226/320/265/321/204/342/225/221/342/225/221/BRAIN_AI/321/206/320/231/320/243/321/205/342/225/226/320/265/321/204/342/225/221/342/225/221Mac_Linux/321/207/320/231/320/230/321/206/320/254/320/274.zip +0 -0
  3. cnhkmcp/untracked/AI/321/206/320/231/320/243/321/205/342/225/226/320/265/321/204/342/225/221/342/225/221//321/205/320/237/320/234/321/205/320/227/342/225/227/321/205/320/276/320/231/321/210/320/263/320/225AI/321/206/320/231/320/243/321/205/342/225/226/320/265/321/204/342/225/221/342/225/221_Windows/321/207/320/231/320/230/321/206/320/254/320/274.exe +0 -0
  4. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/main.py +7 -0
  5. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242//321/211/320/266/320/246/321/206/320/274/320/261/321/210/342/224/220/320/240/321/210/320/261/320/234/321/206/320/231/320/243/321/205/342/225/235/320/220/321/206/320/230/320/241.py +8 -0
  6. cnhkmcp/untracked/APP/ace_lib.py +8 -0
  7. cnhkmcp/untracked/APP/trailSomeAlphas/ace.log +1 -0
  8. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-data-feature-engineering/output_report/GLB_delay1_fundamental28_ideas.md +384 -0
  9. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/final_expressions.json +41 -0
  10. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874844124598400.json +7 -0
  11. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874844589448700.json +8 -0
  12. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874845048996700.json +8 -0
  13. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874845510819100.json +12 -0
  14. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874845978315000.json +10 -0
  15. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874846459411100.json +10 -0
  16. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874846924915700.json +8 -0
  17. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874847399137200.json +8 -0
  18. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874847858960800.json +10 -0
  19. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874848327921300.json +8 -0
  20. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874848810818000.json +8 -0
  21. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874849327754300.json +7 -0
  22. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874849795807500.json +8 -0
  23. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874850272279500.json +8 -0
  24. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874850757124200.json +7 -0
  25. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_1_idea_1769874851224506800.json +8 -0
  26. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/data/fundamental28_GLB_delay1/fundamental28_GLB_delay1.csv +930 -0
  27. cnhkmcp/untracked/APP/trailSomeAlphas/skills/brain-feature-implementation/scripts/ace.log +1 -0
  28. cnhkmcp/untracked/APP//321/210/342/224/220/320/240/321/210/320/261/320/234/321/206/320/231/320/243/321/205/342/225/235/320/220/321/206/320/230/320/241.py +8 -0
  29. cnhkmcp/untracked/arxiv_api.py +7 -0
  30. cnhkmcp/untracked/back_up/forum_functions.py +8 -0
  31. cnhkmcp/untracked/forum_functions.py +8 -0
  32. cnhkmcp/untracked/mcp/321/206/320/246/320/227/321/204/342/225/227/342/225/242/321/210/320/276/342/225/221/321/205/320/255/320/253/321/207/320/231/320/2302_/321/205/320/266/320/222/321/206/320/256/320/254/321/205/320/236/320/257/321/207/320/231/320/230/321/205/320/240/320/277/321/205/320/232/320/270/321/204/342/225/225/320/235/321/204/342/225/221/320/226/321/206/342/225/241/320/237/321/210/320/267/320/230/321/205/320/251/320/270/321/205/342/226/221/342/226/222/321/210/320/277/320/245/321/210/342/224/220/320/251/321/204/342/225/225/320/272/forum_functions.py +8 -0
  33. cnhkmcp/untracked/mcp/321/206/320/246/320/227/321/204/342/225/227/342/225/242/321/210/320/276/342/225/221/321/205/320/255/320/253/321/207/320/231/320/2302_/321/205/320/266/320/222/321/206/320/256/320/254/321/205/320/236/320/257/321/207/320/231/320/230/321/205/320/240/320/277/321/205/320/232/320/270/321/204/342/225/225/320/235/321/204/342/225/221/320/226/321/206/342/225/241/320/237/321/210/320/267/320/230/321/205/320/251/320/270/321/205/342/226/221/342/226/222/321/210/320/277/320/245/321/210/342/224/220/320/251/321/204/342/225/225/320/272/platform_functions.py +8 -1
  34. cnhkmcp/untracked/platform_functions.py +7 -0
  35. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/.gitignore +14 -0
  36. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/SKILL.md +76 -0
  37. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/ace.log +0 -0
  38. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/ace_lib.py +1512 -0
  39. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/config.json +6 -0
  40. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/fundamental28_GLB_1_idea_1769874845978315000.json +10 -0
  41. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/helpful_functions.py +180 -0
  42. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/__init__.py +0 -0
  43. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/build_alpha_list.py +86 -0
  44. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/fetch_sim_options.py +51 -0
  45. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/load_credentials.py +93 -0
  46. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/parse_idea_file.py +85 -0
  47. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/process_template.py +80 -0
  48. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/scripts/resolve_settings.py +94 -0
  49. cnhkmcp/untracked/skills/brain-inspectTemplate-create-Setting/sim_options_snapshot.json +414 -0
  50. cnhkmcp/untracked//321/211/320/225/320/235/321/207/342/225/234/320/276/321/205/320/231/320/235/321/210/342/224/220/320/240/321/210/320/261/320/234/321/206/320/230/320/241_/321/205/320/276/320/231/321/210/320/263/320/225/321/205/342/224/220/320/225/321/210/320/266/320/221/321/204/342/225/233/320/255/321/210/342/225/241/320/246/321/205/320/234/320/225.py +8 -0
  51. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/METADATA +1 -1
  52. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/RECORD +56 -22
  53. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/WHEEL +0 -0
  54. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/entry_points.txt +0 -0
  55. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/licenses/LICENSE +0 -0
  56. {cnhkmcp-2.3.2.dist-info → cnhkmcp-2.3.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1512 @@
1
+ import getpass
2
+ import json
3
+ import logging
4
+ import os
5
+ import threading
6
+ import time
7
+ from functools import partial
8
+ from multiprocessing.pool import ThreadPool
9
+ from pathlib import Path
10
+ from typing import Literal, Optional, Union
11
+ from urllib.parse import urljoin
12
+
13
+ import pandas as pd
14
+ import requests
15
+ import tqdm
16
+ from helpful_functions import (
17
+ expand_dict_columns,
18
+ save_pnl,
19
+ save_simulation_result,
20
+ save_yearly_stats,
21
+ )
22
+
23
+ DEV = False
24
+
25
+
26
+ class SingleSession(requests.Session):
27
+ _instance = None
28
+ _lock = threading.Lock()
29
+ _relogin_lock = threading.Lock()
30
+ _initialized = False
31
+
32
+ def __new__(cls, *args, **kwargs):
33
+ if cls._instance is None:
34
+ with cls._lock:
35
+ if cls._instance is None:
36
+ cls._instance = super().__new__(cls)
37
+ return cls._instance
38
+
39
+ def __init__(self, *args, **kwargs):
40
+ if not self._initialized:
41
+ super(SingleSession, self).__init__(*args, **kwargs)
42
+ self._initialized = True
43
+
44
+ def get_relogin_lock(self):
45
+ return self._relogin_lock
46
+
47
+
48
+ def setup_logger() -> logging.Logger:
49
+ """
50
+ This function sets up a logger that writes log messages to the console and,
51
+ if the global variable DEV is set to True, also to a file named 'ace.log'.
52
+
53
+ Returns:
54
+ logger (logging.Logger): The configured logger object.
55
+
56
+ The logger's name is set to 'ace.log'. The level of the logger and the console handler
57
+ is set to INFO if DEV is True, and WARNING otherwise. The format for the log messages
58
+ is: 'asctime' - 'name' - 'levelname' - 'message'.
59
+ """
60
+ logger = logging.getLogger("ace")
61
+ level = logging.DEBUG if DEV else logging.INFO
62
+
63
+ logger.setLevel(level)
64
+
65
+ console_handler = logging.StreamHandler()
66
+ console_handler.setLevel(level)
67
+
68
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
69
+ console_handler.setFormatter(formatter)
70
+
71
+ logger.addHandler(console_handler)
72
+
73
+ file_handler = logging.FileHandler("ace.log")
74
+ file_handler.setLevel(logging.DEBUG)
75
+ file_handler.setFormatter(formatter)
76
+ logger.addHandler(file_handler)
77
+
78
+ return logger
79
+
80
+
81
+ logger = setup_logger()
82
+
83
+
84
+ DEFAULT_CONFIG = {
85
+ "get_pnl": False,
86
+ "get_stats": False,
87
+ "save_pnl_file": False,
88
+ "save_stats_file": False,
89
+ "save_result_file": False,
90
+ "check_submission": False,
91
+ "check_self_corr": False,
92
+ "check_prod_corr": False,
93
+ }
94
+
95
+ brain_api_url = os.environ.get("BRAIN_API_URL", "https://api.worldquantbrain.com")
96
+
97
+
98
+ def get_credentials() -> tuple[str, str]:
99
+ """
100
+ Retrieve or prompt for platform credentials.
101
+
102
+ This function attempts to read credentials from a JSON file in the user's home directory.
103
+ If the file doesn't exist or is empty, it prompts the user to enter credentials and saves them.
104
+
105
+ Returns:
106
+ tuple: A tuple containing the email and password.
107
+
108
+ Raises:
109
+ json.JSONDecodeError: If the credentials file exists but contains invalid JSON.
110
+ """
111
+
112
+ return ("xxx@xxxx.com", "xxxxxx")
113
+
114
+
115
+ def start_session() -> SingleSession:
116
+ """
117
+ Start a new session with the WorldQuant BRAIN platform.
118
+
119
+ This function authenticates the user, handles biometric authentication if required,
120
+ and creates a new session.
121
+
122
+ Returns:
123
+ SingleSession: An authenticated session object.
124
+
125
+ Raises:
126
+ requests.exceptions.RequestException: If there's an error during the authentication process.
127
+ """
128
+
129
+ s = SingleSession()
130
+ s.auth = get_credentials()
131
+ r = s.post(brain_api_url + "/authentication")
132
+ logger.debug(f"New session created (ID: {id(s)}) with authentication response: {r.status_code}, {r.json()}")
133
+ if r.status_code == requests.status_codes.codes.unauthorized:
134
+ if r.headers["WWW-Authenticate"] == "persona":
135
+ print(
136
+ "Complete biometrics authentication and press any key to continue: \n"
137
+ + urljoin(r.url, r.headers["Location"])
138
+ + "\n"
139
+ )
140
+ input()
141
+ s.post(urljoin(r.url, r.headers["Location"]))
142
+
143
+ while True:
144
+ if s.post(urljoin(r.url, r.headers["Location"])).status_code != 201:
145
+ input(
146
+ "Biometrics authentication is not complete. Please try again and press any key when completed \n"
147
+ )
148
+ else:
149
+ break
150
+ else:
151
+ logger.error("\nIncorrect email or password\n")
152
+ with open(
153
+ os.path.join(os.path.expanduser("~"), "secrets/platform-brain.json"),
154
+ "w",
155
+ ) as file:
156
+ json.dump({}, file)
157
+ return start_session()
158
+ return s
159
+
160
+
161
+ def check_session_timeout(s: SingleSession) -> int:
162
+ """
163
+ Check if the current session has timed out.
164
+
165
+ Args:
166
+ s (SingleSession): The current session object.
167
+
168
+ Returns:
169
+ int: The number of seconds until the session expires, or 0 if the session has expired or an error occurred.
170
+ """
171
+
172
+ authentication_url = brain_api_url + "/authentication"
173
+ try:
174
+ result = s.get(authentication_url).json()["token"]["expiry"]
175
+ logger.debug(f"Session (ID: {id(s)}) timeout check result: {result}")
176
+ return result
177
+ except Exception:
178
+ return 0
179
+
180
+
181
+ def generate_alpha(
182
+ regular: Optional[str] = None,
183
+ selection: Optional[str] = None,
184
+ combo: Optional[str] = None,
185
+ alpha_type: Literal["REGULAR", "SUPER"] = "REGULAR",
186
+ region: str = "USA",
187
+ universe: str = "TOP3000",
188
+ delay: Literal[0, 1] = 1,
189
+ decay: int = 0,
190
+ neutralization: str = "INDUSTRY",
191
+ truncation: float = 0.08,
192
+ pasteurization: Literal["ON", "OFF"] = "ON",
193
+ test_period: str = "P0Y0M0D",
194
+ unit_handling: Literal["VERIFY"] = "VERIFY",
195
+ nan_handling: Literal["ON", "OFF"] = "OFF",
196
+ max_trade: Literal["ON", "OFF"] = "OFF",
197
+ selection_handling: str = "POSITIVE",
198
+ selection_limit: int = 100,
199
+ visualization: bool = False,
200
+ ) -> dict:
201
+ """
202
+ Generate an alpha dictionary for simulation. If alpha_type='REGULAR',
203
+ function generates alpha dictionary using regular input. If alpha_type='SUPER',
204
+ function generates alpha dictionary using selection and combo inputs.
205
+
206
+ Args:
207
+ regular (str, optional): The regular alpha expression.
208
+ selection (str, optional): The selection expression for super alphas.
209
+ combo (str, optional): The combo expression for super alphas.
210
+ alpha_type (str, optional): The type of alpha ("REGULAR" or "SUPER"). Defaults to "REGULAR".
211
+ region (str, optional): The region for the alpha. Defaults to "USA".
212
+ universe (str, optional): The universe for the alpha. Defaults to "TOP3000".
213
+ delay (int, optional): The delay for the alpha. Defaults to 1.
214
+ decay (int, optional): The decay for the alpha. Defaults to 0.
215
+ neutralization (str, optional): The neutralization method. Defaults to "INDUSTRY".
216
+ truncation (float, optional): The truncation value. Defaults to 0.08.
217
+ pasteurization (str, optional): The pasteurization setting. Defaults to "ON".
218
+ test_period (str, optional): The test period. Defaults to "P0Y0M0D".
219
+ unit_handling (str, optional): The unit handling method. Defaults to "VERIFY".
220
+ nan_handling (str, optional): The NaN handling method. Defaults to "OFF".
221
+ max_trade (str, optional): The max trade method. Defaults to "OFF".
222
+ selection_handling (str, optional): The selection handling method for super alphas. Defaults to "POSITIVE".
223
+ selection_limit (int, optional): The selection limit for super alphas. Defaults to 100.
224
+ visualization (bool, optional): Whether to include visualization. Defaults to False.
225
+
226
+ Returns:
227
+ dict: A dictionary containing the alpha configuration for simulation.
228
+
229
+ Raises:
230
+ ValueError: If an invalid alpha_type is provided.
231
+ """
232
+
233
+ settings = {
234
+ "instrumentType": "EQUITY",
235
+ "region": region,
236
+ "universe": universe,
237
+ "delay": delay,
238
+ "decay": decay,
239
+ "neutralization": neutralization,
240
+ "truncation": truncation,
241
+ "pasteurization": pasteurization,
242
+ "testPeriod": test_period,
243
+ "unitHandling": unit_handling,
244
+ "nanHandling": nan_handling,
245
+ "maxTrade": max_trade,
246
+ "language": "FASTEXPR",
247
+ "visualization": visualization,
248
+ }
249
+ if alpha_type == "REGULAR":
250
+ simulation_data = {
251
+ "type": alpha_type,
252
+ "settings": settings,
253
+ "regular": regular,
254
+ }
255
+ elif alpha_type == "SUPER":
256
+ simulation_data = {
257
+ "type": alpha_type,
258
+ "settings": {
259
+ **settings,
260
+ "selectionHandling": selection_handling,
261
+ "selectionLimit": selection_limit,
262
+ },
263
+ "combo": combo,
264
+ "selection": selection,
265
+ }
266
+ else:
267
+ logger.error("alpha_type should be REGULAR or SUPER")
268
+ return {}
269
+ return simulation_data
270
+
271
+
272
+ def check_session_and_relogin(s: SingleSession) -> SingleSession:
273
+ """
274
+ Checks for session timeout and if less than 2000 seconds are remaining,
275
+ it attempts to start a new session.
276
+
277
+ Parameters:
278
+ s (SingleSession): The current session object.
279
+
280
+ Returns:
281
+ s (SingleSession): The original session object if it hasn't timed out,
282
+ otherwise a new session object.
283
+
284
+ If the remaining session time is less than 2000 seconds, the function
285
+ attempts to start a new session using the `start_session()` function.
286
+ If `start_session()` fails on the first attempt, it waits for 100 seconds
287
+ and then tries again. The function then returns the new session object.
288
+ """
289
+ with s.get_relogin_lock():
290
+ if check_session_timeout(s) < 2000:
291
+ logger.debug('Session less than 2000 seconds')
292
+ try:
293
+ s = start_session()
294
+ except Exception:
295
+ logger.info('Trying re-login, wait 100 seconds')
296
+ time.sleep(100)
297
+ s = start_session()
298
+ logger.debug(f"Session (ID: {id(s)}) after check and relogin")
299
+ return s
300
+
301
+
302
+ def start_simulation(s: SingleSession, simulate_data: Union[list[dict], dict]) -> requests.Response:
303
+ """
304
+ Start a simulation with the provided simulation data.
305
+
306
+ Args:
307
+ s (SingleSession): An authenticated session object.
308
+ simulate_data (dict): A dictionary containing the simulation parameters.
309
+
310
+ Returns:
311
+ requests.Response: The response object from the simulation start request.
312
+
313
+ Raises:
314
+ requests.exceptions.RequestException: If there's an error in the API request.
315
+ """
316
+ simulate_response = s.post(brain_api_url + "/simulations", json=simulate_data)
317
+ return simulate_response
318
+
319
+
320
+ def simulation_progress(
321
+ s: SingleSession,
322
+ simulate_response: requests.Response,
323
+ ) -> dict:
324
+ """
325
+ Monitor the progress of a simulation and return the result when complete.
326
+
327
+ Args:
328
+ s (SingleSession): An authenticated session object.
329
+ simulate_response (requests.Response): The response from starting the simulation.
330
+
331
+ Returns:
332
+ dict: A dictionary containing the completion status and simulation result.
333
+
334
+ Raises:
335
+ requests.exceptions.RequestException: If there's an error in the API requests.
336
+ """
337
+ if simulate_response.status_code // 100 != 2:
338
+ logger.warning(f'Simulation failed. {simulate_response.text}, Status code: {simulate_response.status_code}')
339
+ return {"completed": False, "result": {}}
340
+
341
+ simulation_progress_url = simulate_response.headers["Location"]
342
+ error_flag = False
343
+ retry_count = 0
344
+ while True:
345
+ simulation_progress_response = s.get(simulation_progress_url)
346
+ if simulation_progress_response.status_code // 100 != 2:
347
+ logger.error(
348
+ f'Simulation {simulation_progress_url}, Status code: {simulation_progress_response.status_code}, Retry'
349
+ )
350
+ time.sleep(30)
351
+ retry_count += 1
352
+ if retry_count <= 2:
353
+ continue
354
+ else:
355
+ logger.error(
356
+ f'Simulation {simulation_progress_url} failed, Status code: {simulation_progress_response.status_code}'
357
+ )
358
+ error_flag = True
359
+ break
360
+ if simulation_progress_response.headers.get("Retry-After", 0) == 0:
361
+ if simulation_progress_response.json().get("status", "ERROR") == "ERROR":
362
+ error_flag = True
363
+ break
364
+
365
+ time.sleep(float(simulation_progress_response.headers["Retry-After"]))
366
+
367
+ if error_flag:
368
+ logger.error(f"Simulation failed. {simulation_progress_response.json()}")
369
+ return {"completed": False, "result": {}}
370
+
371
+ alpha = simulation_progress_response.json().get("alpha", 0)
372
+ if alpha == 0:
373
+ logger.warning(
374
+ f'Simulation {simulation_progress_response.json().get("id")} failed. {simulation_progress_response.json()}'
375
+ )
376
+ return {"completed": False, "result": {}}
377
+ simulation_result = get_simulation_result_json(s, alpha)
378
+ if len(simulation_result) == 0:
379
+ return {"completed": False, "result": {}}
380
+ return {"completed": True, "result": simulation_result}
381
+
382
+
383
+ def get_simulation_result_json(s: SingleSession, alpha_id: str) -> dict:
384
+ """
385
+ Retrieve the full simulation result for a specific alpha.
386
+
387
+ Args:
388
+ s (SingleSession): An authenticated session object.
389
+ alpha_id (str): The ID of the alpha.
390
+
391
+ Returns:
392
+ dict: A dictionary containing the full simulation result.
393
+
394
+ Raises:
395
+ requests.exceptions.RequestException: If there's an error in the API request.
396
+ """
397
+ if alpha_id is None:
398
+ return {}
399
+ while True:
400
+ result = s.get(brain_api_url + "/alphas/" + alpha_id)
401
+ if "retry-after" in result.headers:
402
+ time.sleep(float(result.headers["Retry-After"]))
403
+ else:
404
+ break
405
+ try:
406
+ return result.json()
407
+ except Exception:
408
+ logger.error(f"alpha_id {alpha_id}, {result.headers}, {result.text}, {result.status_code}")
409
+ return {}
410
+ return s.get(brain_api_url + "/alphas/" + alpha_id).json()
411
+
412
+
413
+ def multisimulation_progress(
414
+ s: SingleSession,
415
+ simulate_response: requests.Response,
416
+ ) -> dict:
417
+ """
418
+ Monitor the progress of multiple simulations and return the results when complete.
419
+
420
+ Args:
421
+ s (SingleSession): An authenticated session object.
422
+ simulate_response (requests.Response): The response from starting the simulations.
423
+
424
+ Returns:
425
+ dict: A dictionary containing the completion status and simulation results.
426
+
427
+ Raises:
428
+ requests.exceptions.RequestException: If there's an error in the API requests.
429
+ """
430
+ if simulate_response.status_code // 100 != 2:
431
+ logger.warning(f'Simulation failed. {simulate_response.text}, Status code: {simulate_response.status_code}')
432
+ return {"completed": False, "result": {}}
433
+
434
+ simulation_progress_url = simulate_response.headers["Location"]
435
+ error_flag = False
436
+ while True:
437
+ simulation_progress_response = s.get(simulation_progress_url)
438
+ if simulation_progress_response.status_code // 100 != 2:
439
+ time.sleep(30)
440
+ if simulation_progress_response.headers.get("Retry-After", 0) == 0:
441
+ if simulation_progress_response.json().get("status", "ERROR") == "ERROR":
442
+ error_flag = True
443
+ break
444
+
445
+ time.sleep(float(simulation_progress_response.headers["Retry-After"]))
446
+
447
+ children = simulation_progress_response.json().get("children", 0)
448
+
449
+ if error_flag:
450
+ if children == 0:
451
+ logger.error(f"Simulation failed. {simulation_progress_response.json()}")
452
+ return {"completed": False, "result": {}}
453
+ for child in children:
454
+ child_progress = s.get(brain_api_url + "/simulations/" + child)
455
+ logger.error(f"Child Simulation failed: {child_progress.json()}")
456
+ return {"completed": False, "result": {}}
457
+
458
+ if len(children) == 0:
459
+ logger.warning(
460
+ f'Multi-Simulation {simulation_progress_response.json().get("id")} failed. {simulation_progress_response.json()}'
461
+ )
462
+ return {"completed": False, "result": {}}
463
+ children_list = []
464
+ for child in children:
465
+ child_progress = s.get(brain_api_url + "/simulations/" + child)
466
+ alpha = child_progress.json().get("alpha", 0)
467
+ if alpha == 0:
468
+ logger.warning(f'Child-Simulation {child_progress.json().get("id")} failed. {child_progress.json()}')
469
+ return {"completed": False, "result": {}}
470
+ child_result = get_simulation_result_json(s, alpha)
471
+ children_list.append(child_result)
472
+ return {"completed": True, "result": children_list}
473
+
474
+
475
+ def get_prod_corr(s: SingleSession, alpha_id: str) -> pd.DataFrame:
476
+ """
477
+ Retrieve the production correlation data for a specific alpha.
478
+
479
+ Args:
480
+ s (SingleSession): An authenticated session object.
481
+ alpha_id (str): The ID of the alpha.
482
+
483
+ Returns:
484
+ pandas.DataFrame: A DataFrame containing the production correlation data.
485
+
486
+ Raises:
487
+ requests.exceptions.RequestException: If there's an error in the API request.
488
+ """
489
+
490
+ while True:
491
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + "/correlations/prod")
492
+ if "retry-after" in result.headers:
493
+ time.sleep(float(result.headers["Retry-After"]))
494
+ else:
495
+ break
496
+ if result.json().get("records", 0) == 0:
497
+ logger.warning(f"Failed to get production correlation for alpha_id {alpha_id}. {result.json()}")
498
+ return pd.DataFrame()
499
+ columns = [dct["name"] for dct in result.json()["schema"]["properties"]]
500
+ prod_corr_df = pd.DataFrame(result.json()["records"], columns=columns).assign(alpha_id=alpha_id)
501
+ prod_corr_df["alpha_max_prod_corr"] = result.json()["max"]
502
+ prod_corr_df["alpha_min_prod_corr"] = result.json()["min"]
503
+
504
+ return prod_corr_df
505
+
506
+
507
+ def check_prod_corr_test(s: SingleSession, alpha_id: str, threshold: float = 0.7) -> pd.DataFrame:
508
+ """
509
+ Check if the alpha's production correlation passes a specified threshold.
510
+
511
+ Args:
512
+ s (SingleSession): An authenticated session object.
513
+ alpha_id (str): The ID of the alpha.
514
+ threshold (float, optional): The correlation threshold. Defaults to 0.7.
515
+
516
+ Returns:
517
+ pandas.DataFrame: A DataFrame containing the test result.
518
+
519
+ Raises:
520
+ requests.exceptions.RequestException: If there's an error in the API request.
521
+ """
522
+
523
+ prod_corr_df = get_prod_corr(s, alpha_id)
524
+ if prod_corr_df.empty:
525
+ result = [
526
+ {
527
+ "test": "PROD_CORRELATION",
528
+ "result": "NONE",
529
+ "limit": threshold,
530
+ "value": None,
531
+ "alpha_id": alpha_id,
532
+ }
533
+ ]
534
+ else:
535
+ value = prod_corr_df[prod_corr_df.alphas > 0]["max"].max()
536
+ result = [
537
+ {
538
+ "test": "PROD_CORRELATION",
539
+ "result": "PASS" if value <= threshold else "FAIL",
540
+ "limit": threshold,
541
+ "value": value,
542
+ "alpha_id": alpha_id,
543
+ }
544
+ ]
545
+ return pd.DataFrame(result)
546
+
547
+
548
+ def get_self_corr(s: SingleSession, alpha_id: str) -> pd.DataFrame:
549
+ """
550
+ Retrieve the self-correlation data for a specific alpha.
551
+
552
+ Args:
553
+ s (SingleSession): An authenticated session object.
554
+ alpha_id (str): The ID of the alpha.
555
+
556
+ Returns:
557
+ pandas.DataFrame: A DataFrame containing the self-correlation data.
558
+
559
+ Raises:
560
+ requests.exceptions.RequestException: If there's an error in the API request.
561
+ """
562
+
563
+ while True:
564
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + "/correlations/self")
565
+ if "retry-after" in result.headers:
566
+ time.sleep(float(result.headers["Retry-After"]))
567
+ else:
568
+ break
569
+ if result.json().get("records", 0) == 0:
570
+ logger.warning(f"Failed to get self correlation for alpha_id {alpha_id}. {result.json()}")
571
+ return pd.DataFrame()
572
+
573
+ records_len = len(result.json()["records"])
574
+ if records_len == 0:
575
+ logger.warning(f"No self correlation for alpha_id {alpha_id}")
576
+ return pd.DataFrame()
577
+
578
+ columns = [dct["name"] for dct in result.json()["schema"]["properties"]]
579
+ self_corr_df = pd.DataFrame(result.json()["records"], columns=columns).assign(alpha_id=alpha_id)
580
+ self_corr_df["alpha_max_self_corr"] = result.json()["max"]
581
+ self_corr_df["alpha_min_self_corr"] = result.json()["min"]
582
+
583
+ return self_corr_df
584
+
585
+
586
+ def check_self_corr_test(s: SingleSession, alpha_id: str, threshold: float = 0.7) -> pd.DataFrame:
587
+ """
588
+ Check if the alpha's self-correlation passes a specified threshold.
589
+
590
+ Args:
591
+ s (SingleSession): An authenticated session object.
592
+ alpha_id (str): The ID of the alpha.
593
+ threshold (float, optional): The correlation threshold. Defaults to 0.7.
594
+
595
+ Returns:
596
+ pandas.DataFrame: A DataFrame containing the test result.
597
+
598
+ Raises:
599
+ requests.exceptions.RequestException: If there's an error in the API request.
600
+ """
601
+
602
+ self_corr_df = get_self_corr(s, alpha_id)
603
+ if self_corr_df.empty:
604
+ result = [
605
+ {
606
+ "test": "SELF_CORRELATION",
607
+ "result": "PASS",
608
+ "limit": threshold,
609
+ "value": 0,
610
+ "alpha_id": alpha_id,
611
+ }
612
+ ]
613
+ else:
614
+ value = self_corr_df["correlation"].max()
615
+ result = [
616
+ {
617
+ "test": "SELF_CORRELATION",
618
+ "result": "PASS" if value < threshold else "FAIL",
619
+ "limit": threshold,
620
+ "value": value,
621
+ "alpha_id": alpha_id,
622
+ }
623
+ ]
624
+ return pd.DataFrame(result)
625
+
626
+
627
+ def get_check_submission(s: SingleSession, alpha_id: str) -> pd.DataFrame:
628
+ """
629
+ Retrieve the submission check results for a specific alpha.
630
+
631
+ Args:
632
+ s (SingleSession): An authenticated session object.
633
+ alpha_id (str): The ID of the alpha.
634
+
635
+ Returns:
636
+ pandas.DataFrame: A DataFrame containing the submission check results.
637
+
638
+ Raises:
639
+ requests.exceptions.RequestException: If there's an error in the API request.
640
+ """
641
+
642
+ while True:
643
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + "/check")
644
+ if "retry-after" in result.headers:
645
+ time.sleep(float(result.headers["Retry-After"]))
646
+ else:
647
+ break
648
+ if result.json().get("is", 0) == 0:
649
+ logger.warning(f"Cant check submission alpha_id {alpha_id}. {result.json()}")
650
+ return pd.DataFrame()
651
+
652
+ checks_df = pd.DataFrame(result.json()["is"]["checks"]).assign(alpha_id=alpha_id)
653
+
654
+ return checks_df
655
+
656
+
657
+ def simulate_multi_alpha(
658
+ s: SingleSession,
659
+ simulate_data_list: list,
660
+ tags: Optional[list[str]] = None,
661
+ ) -> list[dict]:
662
+ """
663
+ Simulate a list of alphas using multi-simulation.
664
+
665
+ This function checks the session timeout, starts a new session if necessary,
666
+ initiates the simulation, monitors its progress, and sets alpha properties
667
+ upon completion.
668
+
669
+ Args:
670
+ s (SingleSession): An authenticated session object.
671
+ simulate_data (dict): A list of dictionaries, each containing the simulation parameters for the alpha.
672
+ These should include all necessary information such as alpha type, settings, and expressions.
673
+
674
+ Returns:
675
+ list: A list of dictionaries, each containing:
676
+ - 'alpha_id' (str): The ID of the simulated alpha if successful, None otherwise.
677
+ - 'simulate_data' (dict): The original simulation data provided.
678
+
679
+ Raises:
680
+ requests.exceptions.RequestException: If there's an error in the API requests.
681
+ """
682
+
683
+ s = check_session_and_relogin(s)
684
+ if len(simulate_data_list) == 1:
685
+ return [simulate_single_alpha(s, simulate_data_list[0])]
686
+ simulate_response = start_simulation(s, simulate_data_list)
687
+ simulation_result = multisimulation_progress(s, simulate_response)
688
+
689
+ if not simulation_result["completed"]:
690
+ return [{"alpha_id": None, "simulate_data": x} for x in simulate_data_list]
691
+ result = [
692
+ {
693
+ "alpha_id": x["id"],
694
+ "simulate_data": {
695
+ "type": x["type"],
696
+ "settings": x["settings"],
697
+ "regular": x["regular"]["code"],
698
+ },
699
+ }
700
+ for x in simulation_result["result"]
701
+ ]
702
+ if tags:
703
+ _ = [set_alpha_properties(s, x["id"], tags=tags) for x in simulation_result["result"]]
704
+ else:
705
+ _ = [set_alpha_properties(s, x["id"]) for x in simulation_result["result"]]
706
+ return result
707
+
708
+
709
+ def get_specified_alpha_stats(
710
+ s: SingleSession,
711
+ alpha_id: Union[str, None],
712
+ simulate_data: dict,
713
+ get_pnl: bool = False,
714
+ get_stats: bool = False,
715
+ save_pnl_file: bool = False,
716
+ save_stats_file: bool = False,
717
+ save_result_file: bool = False,
718
+ check_submission: bool = False,
719
+ check_self_corr: bool = False,
720
+ check_prod_corr: bool = False,
721
+ ) -> dict:
722
+ """
723
+ Retrieve and process specified statistics for a given alpha.
724
+
725
+ Args:
726
+ s (SingleSession): The authenticated session object.
727
+ alpha_id (str): The ID of the alpha to retrieve statistics for.
728
+ simulate_data (dict): The original simulation data for the alpha.
729
+ get_pnl (bool, optional): Whether to retrieve PnL data. Defaults to False.
730
+ get_stats (bool, optional): Whether to retrieve yearly stats. Defaults to False.
731
+ save_pnl_file (bool, optional): Whether to save PnL data to a file. Defaults to False.
732
+ save_stats_file (bool, optional): Whether to save yearly stats to a file. Defaults to False.
733
+ save_result_file (bool, optional): Whether to save the simulation result to a file. Defaults to False.
734
+ check_submission (bool, optional): Whether to perform submission checks. Defaults to False.
735
+ check_self_corr (bool, optional): Whether to check self-correlation. Defaults to False.
736
+ check_prod_corr (bool, optional): Whether to check production correlation. Defaults to False.
737
+
738
+ Returns:
739
+ dict: A dictionary containing various statistics and information about the alpha.
740
+
741
+ Raises:
742
+ requests.exceptions.RequestException: If there's an error retrieving data from the API.
743
+ """
744
+ pnl = None
745
+ stats = None
746
+ s = check_session_and_relogin(s)
747
+ logger.debug(f"Session (ID: {id(s)}) used in get_specified_alpha_stats for alpha_id: {alpha_id}")
748
+ if alpha_id is None:
749
+ return {
750
+ "alpha_id": None,
751
+ "simulate_data": simulate_data,
752
+ "is_stats": None,
753
+ "pnl": pnl,
754
+ "stats": stats,
755
+ "is_tests": None,
756
+ "train": None,
757
+ "test": None,
758
+ }
759
+
760
+ result = get_simulation_result_json(s, alpha_id)
761
+ try:
762
+ region = result["settings"]["region"]
763
+ is_stats = pd.DataFrame([{key: value for key, value in result['is'].items() if key != 'checks'}]).assign(
764
+ alpha_id=alpha_id
765
+ )
766
+ except Exception as e:
767
+ logger.error(f"Failed to retrieve simulation result for alpha_id {alpha_id}: {result}, {e}")
768
+ train = result["train"]
769
+ test = result["test"]
770
+ is_stats = pd.DataFrame([{key: value for key, value in result["is"].items() if key != "checks"}]).assign(
771
+ alpha_id=alpha_id
772
+ )
773
+
774
+ if get_pnl:
775
+ pnl = get_alpha_pnl(s, alpha_id)
776
+ if save_pnl_file:
777
+ save_pnl(pnl, alpha_id, region)
778
+
779
+ if get_stats:
780
+ stats = get_alpha_yearly_stats(s, alpha_id)
781
+ if save_stats_file:
782
+ save_yearly_stats(stats, alpha_id, region)
783
+
784
+ if save_result_file:
785
+ save_simulation_result(result)
786
+
787
+ is_tests = pd.DataFrame(result["is"]["checks"]).assign(alpha_id=alpha_id)
788
+
789
+ if check_submission:
790
+ is_tests = get_check_submission(s, alpha_id)
791
+
792
+ return {
793
+ "alpha_id": alpha_id,
794
+ "simulate_data": simulate_data,
795
+ "is_stats": is_stats,
796
+ "pnl": pnl,
797
+ "stats": stats,
798
+ "is_tests": is_tests,
799
+ "train": train,
800
+ "test": test,
801
+ }
802
+
803
+ if check_self_corr and not check_submission:
804
+ self_corr_test = check_self_corr_test(s, alpha_id)
805
+ is_tests = (
806
+ pd.concat([is_tests, pd.DataFrame([self_corr_test])], ignore_index=True)
807
+ .drop_duplicates(subset=["test"], keep="last")
808
+ .reset_index(drop=True)
809
+ )
810
+ if check_prod_corr and not check_submission:
811
+ prod_corr_test = check_prod_corr_test(s, alpha_id)
812
+ is_tests = (
813
+ pd.concat([is_tests, pd.DataFrame([prod_corr_test])], ignore_index=True)
814
+ .drop_duplicates(subset=["test"], keep="last")
815
+ .reset_index(drop=True)
816
+ )
817
+
818
+ return {
819
+ "alpha_id": alpha_id,
820
+ "simulate_data": simulate_data,
821
+ "is_stats": is_stats,
822
+ "pnl": pnl,
823
+ "stats": stats,
824
+ "is_tests": is_tests,
825
+ "train": train,
826
+ "test": test,
827
+ }
828
+
829
+
830
+ def simulate_single_alpha(
831
+ s: SingleSession,
832
+ simulate_data: dict,
833
+ ) -> dict:
834
+ """
835
+ Simulate a single alpha using the provided session and simulation data.
836
+
837
+ This function checks the session timeout, starts a new session if necessary,
838
+ initiates the simulation, monitors its progress, and sets alpha properties
839
+ upon completion.
840
+
841
+ Args:
842
+ s (SingleSession): An authenticated session object.
843
+ simulate_data (dict): A dictionary containing the simulation parameters for the alpha.
844
+ This should include all necessary information such as alpha type, settings, and expressions.
845
+
846
+ Returns:
847
+ dict: A dictionary containing:
848
+ - 'alpha_id' (str): The ID of the simulated alpha if successful, None otherwise.
849
+ - 'simulate_data' (dict): The original simulation data provided.
850
+
851
+ Raises:
852
+ requests.exceptions.RequestException: If there's an error in the API requests.
853
+ """
854
+
855
+ s = check_session_and_relogin(s)
856
+ simulate_response = start_simulation(s, simulate_data)
857
+ simulation_result = simulation_progress(s, simulate_response)
858
+
859
+ if not simulation_result["completed"]:
860
+ return {"alpha_id": None, "simulate_data": simulate_data}
861
+ set_alpha_properties(s, simulation_result["result"]["id"])
862
+ return {
863
+ "alpha_id": simulation_result["result"]["id"],
864
+ "simulate_data": simulate_data,
865
+ }
866
+
867
+
868
+ def simulate_alpha_list(
869
+ s: SingleSession,
870
+ alpha_list: list,
871
+ limit_of_concurrent_simulations: int = 3,
872
+ simulation_config: dict = DEFAULT_CONFIG,
873
+ ) -> list:
874
+ """
875
+ Simulate a list of alphas concurrently.
876
+
877
+ Args:
878
+ s (SingleSession): The authenticated session object.
879
+ alpha_list (list): A list of alpha configurations to simulate.
880
+ limit_of_concurrent_simulations (int, optional): The maximum number of concurrent simulations. Defaults to 3.
881
+ simulation_config (dict, optional): Configuration for the simulation. Defaults to DEFAULT_CONFIG.
882
+
883
+ Returns:
884
+ list: A list of dictionaries containing simulation results for each alpha.
885
+
886
+ Raises:
887
+ requests.exceptions.RequestException: If there's an error during the simulation process.
888
+ """
889
+ if (limit_of_concurrent_simulations < 1) or (limit_of_concurrent_simulations > 8):
890
+ logger.warning("Limit of concurrent simulation should be 1..8, will be set to 3")
891
+ limit_of_concurrent_simulations = 3
892
+
893
+ result_list = []
894
+
895
+ with ThreadPool(limit_of_concurrent_simulations) as pool:
896
+ with tqdm.tqdm(total=len(alpha_list)) as pbar:
897
+ for result in pool.imap_unordered(partial(simulate_single_alpha, s), alpha_list):
898
+ result_list.append(result)
899
+ pbar.update()
900
+
901
+ stats_list_result = []
902
+
903
+ def func(x):
904
+ return get_specified_alpha_stats(s, x["alpha_id"], x["simulate_data"], **simulation_config)
905
+
906
+ with ThreadPool(3) as pool:
907
+ for result in pool.map(func, result_list):
908
+ stats_list_result.append(result)
909
+
910
+ return _delete_duplicates_from_result(stats_list_result)
911
+
912
+
913
+ def simulate_alpha_list_multi(
914
+ s: SingleSession,
915
+ alpha_list: list,
916
+ limit_of_concurrent_simulations: int = 3,
917
+ limit_of_multi_simulations: int = 3,
918
+ simulation_config: dict = DEFAULT_CONFIG,
919
+ tags: Optional[list[str]] = None,
920
+ ) -> list:
921
+ """
922
+ Simulate a list of alphas using multi-simulation when possible.
923
+
924
+ Args:
925
+ s (SingleSession): An authenticated session object.
926
+ alpha_list (list): A list of alpha configurations to simulate.
927
+ limit_of_concurrent_simulations (int, optional): The maximum number of concurrent simulation batches. Defaults to 3.
928
+ limit_of_multi_simulations (int, optional): The maximum number of alphas in a multi-simulation. Defaults to 3.
929
+ simulation_config (dict, optional): Configuration for the simulation. Defaults to DEFAULT_CONFIG.
930
+
931
+ Returns:
932
+ list: A list of dictionaries containing simulation results for each alpha.
933
+
934
+ Raises:
935
+ requests.exceptions.RequestException: If there's an error in the API requests.
936
+ """
937
+ if (limit_of_multi_simulations < 2) or (limit_of_multi_simulations > 10):
938
+ logger.warning("Limit of multi-simulation should be 2..10, will be set to 3")
939
+ limit_of_multi_simulations = 3
940
+ if (limit_of_concurrent_simulations < 1) or (limit_of_concurrent_simulations > 8):
941
+ logger.warning("Limit of concurrent simulation should be 1..8, will be set to 3")
942
+ limit_of_concurrent_simulations = 3
943
+ if len(alpha_list) < 10:
944
+ logger.warning(
945
+ "List of alphas too short, single concurrent simulations will be used instead of multisimulations"
946
+ )
947
+ return simulate_alpha_list(
948
+ s,
949
+ alpha_list,
950
+ limit_of_concurrent_simulations=limit_of_concurrent_simulations,
951
+ simulation_config=simulation_config,
952
+ )
953
+ if any(d["type"] == "SUPER" for d in alpha_list):
954
+ logger.warning("Multi-Simulation is not supported for SuperAlphas, single concurrent simulations will be used")
955
+ return simulate_alpha_list(
956
+ s,
957
+ alpha_list,
958
+ limit_of_concurrent_simulations=3,
959
+ simulation_config=simulation_config,
960
+ )
961
+
962
+ tasks = [
963
+ alpha_list[i : i + limit_of_multi_simulations] for i in range(0, len(alpha_list), limit_of_multi_simulations)
964
+ ]
965
+ result_list = []
966
+
967
+ with ThreadPool(limit_of_concurrent_simulations) as pool:
968
+ with tqdm.tqdm(total=len(tasks)) as pbar:
969
+ for result in pool.imap_unordered(partial(simulate_multi_alpha, s, tags=tags), tasks):
970
+ result_list.append(result)
971
+ pbar.update()
972
+ result_list_flat = [item for sublist in result_list for item in sublist]
973
+
974
+ stats_list_result = []
975
+
976
+ def func(x):
977
+ return get_specified_alpha_stats(s, x["alpha_id"], x["simulate_data"], **simulation_config)
978
+
979
+ with ThreadPool(3) as pool:
980
+ for result in pool.map(func, result_list_flat):
981
+ stats_list_result.append(result)
982
+
983
+ return _delete_duplicates_from_result(stats_list_result)
984
+
985
+
986
+ def _delete_duplicates_from_result(result: list) -> list:
987
+ """
988
+ Remove duplicate alpha results from the simulation output.
989
+
990
+ Args:
991
+ result (list): A list of dictionaries containing simulation results.
992
+
993
+ Returns:
994
+ list: A deduplicated list of simulation results.
995
+ """
996
+ alpha_id_lst = []
997
+ result_new = []
998
+ for x in result:
999
+ if x["alpha_id"] is not None:
1000
+ if x["alpha_id"] not in alpha_id_lst:
1001
+ result_new.append(x)
1002
+ alpha_id_lst.append(x["alpha_id"])
1003
+ else:
1004
+ result_new.append(x)
1005
+ return result_new
1006
+
1007
+
1008
+ def set_alpha_properties(
1009
+ s: SingleSession,
1010
+ alpha_id: str,
1011
+ name: Optional[str] = None,
1012
+ color: Optional[str] = None,
1013
+ regular_desc: Optional[str] = None,
1014
+ selection_desc: str = "None",
1015
+ combo_desc: str = "None",
1016
+ tags: Optional[list[str]] = None,
1017
+ ) -> requests.Response:
1018
+ """
1019
+ Update the properties of an alpha.
1020
+
1021
+ Args:
1022
+ s (SingleSession): An authenticated session object.
1023
+ alpha_id (str): The ID of the alpha to update.
1024
+ name (str, optional): The new name for the alpha. Defaults to None.
1025
+ color (str, optional): The new color for the alpha. Defaults to None.
1026
+ regular_desc (str, optional): Description for regular alpha. Defaults to None.
1027
+ selection_desc (str, optional): Description for the selection part of a super alpha. Defaults to "None".
1028
+ combo_desc (str, optional): Description for the combo part of a super alpha. Defaults to "None".
1029
+ tags (list, optional): List of tags to apply to the alpha. Defaults to None.
1030
+
1031
+ Returns:
1032
+ requests.Response: The response object from the API call.
1033
+ """
1034
+
1035
+ params = {}
1036
+ if name is not None:
1037
+ params["name"] = name
1038
+ if color is not None:
1039
+ params["color"] = color
1040
+ if tags is not None:
1041
+ params["tags"] = tags
1042
+ if regular_desc is not None:
1043
+ params.setdefault("regular", {})["description"] = regular_desc
1044
+ if selection_desc != "None": # Assuming "None" is the default string value for selection_desc
1045
+ params.setdefault("selection", {})["description"] = selection_desc
1046
+ if combo_desc != "None": # Assuming "None" is the default string value for combo_desc
1047
+ params.setdefault("combo", {})["description"] = combo_desc
1048
+
1049
+ response = s.patch(brain_api_url + "/alphas/" + alpha_id, json=params)
1050
+
1051
+ return response
1052
+
1053
+
1054
+ def _get_alpha_pnl(
1055
+ s: SingleSession,
1056
+ alpha_id: str,
1057
+ pnl_type: str = "pnl",
1058
+ ) -> pd.DataFrame:
1059
+ """
1060
+ Retrieve the PnL data for a specific alpha.
1061
+
1062
+ Args:
1063
+ s (SingleSession): An authenticated session object.
1064
+ alpha_id (str): The ID of the alpha.
1065
+ pnl_type (str): 'pnl' to get cumulative pnl, 'daily-pnl' to get daily pnl.
1066
+
1067
+ Returns:
1068
+ pandas.DataFrame: A DataFrame containing the PnL data for the alpha.
1069
+ """
1070
+
1071
+ while True:
1072
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + f"/recordsets/{pnl_type}")
1073
+ if "retry-after" in result.headers:
1074
+ time.sleep(float(result.headers["Retry-After"]))
1075
+ else:
1076
+ break
1077
+ pnl = result.json()
1078
+ if pnl.get("records", 0) == 0:
1079
+ return pd.DataFrame()
1080
+ columns = [dct["name"] for dct in pnl["schema"]["properties"]]
1081
+ pnl_df = (
1082
+ pd.DataFrame(pnl["records"], columns=columns)
1083
+ .assign(alpha_id=alpha_id, date=lambda x: pd.to_datetime(x.date, format="%Y-%m-%d"))
1084
+ .set_index("date")
1085
+ )
1086
+ return pnl_df
1087
+
1088
+
1089
+ def get_alpha_pnl(s: SingleSession, alpha_id: str) -> pd.DataFrame:
1090
+ """
1091
+ Retrieve the cumulative PnL data for a specific alpha.
1092
+
1093
+ Args:
1094
+ s (SingleSession): An authenticated session object.
1095
+ alpha_id (str): The ID of the alpha.
1096
+
1097
+ Returns:
1098
+ pandas.DataFrame: A DataFrame containing the PnL data for the alpha.
1099
+ """
1100
+
1101
+ return _get_alpha_pnl(s, alpha_id, "pnl")
1102
+
1103
+
1104
+ def get_alpha_yearly_stats(s: SingleSession, alpha_id: str) -> pd.DataFrame:
1105
+ """
1106
+ Retrieve the yearly statistics for a specific alpha.
1107
+
1108
+ Args:
1109
+ s (SingleSession): An authenticated session object.
1110
+ alpha_id (str): The ID of the alpha.
1111
+
1112
+ Returns:
1113
+ pandas.DataFrame: A DataFrame containing the yearly statistics for the alpha.
1114
+ """
1115
+
1116
+ while True:
1117
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + "/recordsets/yearly-stats")
1118
+ if "retry-after" in result.headers:
1119
+ time.sleep(float(result.headers["Retry-After"]))
1120
+ else:
1121
+ break
1122
+ stats = result.json()
1123
+
1124
+ if stats.get("records", 0) == 0:
1125
+ return pd.DataFrame()
1126
+ columns = [dct["name"] for dct in stats["schema"]["properties"]]
1127
+ yearly_stats_df = pd.DataFrame(stats["records"], columns=columns).assign(alpha_id=alpha_id)
1128
+ return yearly_stats_df
1129
+
1130
+
1131
+ def get_datasets(
1132
+ s: SingleSession,
1133
+ instrument_type: str = "EQUITY",
1134
+ region: str = "USA",
1135
+ delay: int = 1,
1136
+ universe: str = "TOP3000",
1137
+ theme: str = "ALL",
1138
+ ) -> pd.DataFrame:
1139
+ """
1140
+ Retrieve available datasets based on specified parameters.
1141
+
1142
+ Args:
1143
+ s (SingleSession): An authenticated session object.
1144
+ instrument_type (str, optional): The type of instrument. Defaults to "EQUITY".
1145
+ region (str, optional): The region. Defaults to "USA".
1146
+ delay (int, optional): The delay. Defaults to 1.
1147
+ universe (str, optional): The universe. Defaults to "TOP3000".
1148
+ theme (str, optional): The theme. Defaults to "ALL".
1149
+
1150
+ Returns:
1151
+ pandas.DataFrame: A DataFrame containing information about available datasets.
1152
+ """
1153
+ if theme == "ALL":
1154
+ # Fetch both theme=false and theme=true
1155
+ url_false = (
1156
+ brain_api_url
1157
+ + "/data-sets?"
1158
+ + f"instrumentType={instrument_type}&region={region}&delay={str(delay)}&universe={universe}&theme=false"
1159
+ )
1160
+ result_false = s.get(url_false)
1161
+ df_false = pd.DataFrame(result_false.json()["results"])
1162
+
1163
+ url_true = (
1164
+ brain_api_url
1165
+ + "/data-sets?"
1166
+ + f"instrumentType={instrument_type}&region={region}&delay={str(delay)}&universe={universe}&theme=true"
1167
+ )
1168
+ result_true = s.get(url_true)
1169
+ df_true = pd.DataFrame(result_true.json()["results"])
1170
+
1171
+ datasets_df = pd.concat([df_false, df_true], ignore_index=True)
1172
+ else:
1173
+ url = (
1174
+ brain_api_url
1175
+ + "/data-sets?"
1176
+ + f"instrumentType={instrument_type}&region={region}&delay={str(delay)}&universe={universe}&theme={theme}"
1177
+ )
1178
+ result = s.get(url)
1179
+ datasets_df = pd.DataFrame(result.json()["results"])
1180
+
1181
+ datasets_df = expand_dict_columns(datasets_df)
1182
+ return datasets_df
1183
+
1184
+
1185
+ def get_datafields(
1186
+ s: SingleSession,
1187
+ instrument_type: str = "EQUITY",
1188
+ region: str = "USA",
1189
+ delay: int = 1,
1190
+ universe: str = "TOP3000",
1191
+ theme: str = "false",
1192
+ dataset_id: str = "",
1193
+ data_type: str = "MATRIX",
1194
+ search: str = "",
1195
+ ) -> pd.DataFrame:
1196
+ """
1197
+ Retrieve available datafields based on specified parameters.
1198
+
1199
+ Args:
1200
+ s (SingleSession): An authenticated session object.
1201
+ instrument_type (str, optional): The type of instrument. Defaults to "EQUITY".
1202
+ region (str, optional): The region. Defaults to "USA".
1203
+ delay (int, optional): The delay. Defaults to 1.
1204
+ universe (str, optional): The universe. Defaults to "TOP3000".
1205
+ theme (str, optional): The theme. Defaults to "false".
1206
+ dataset_id (str, optional): The ID of a specific dataset. Defaults to "".
1207
+ data_type (str, optional): The type of data. Defaults to "MATRIX".
1208
+ search (str, optional): A search string to filter datafields. Defaults to "".
1209
+
1210
+ Returns:
1211
+ pandas.DataFrame: A DataFrame containing information about available datafields.
1212
+ """
1213
+ type_param = f"&type={data_type}" if data_type != "ALL" else ""
1214
+ if len(search) == 0:
1215
+ url_template = (
1216
+ brain_api_url
1217
+ + "/data-fields?"
1218
+ + f"&instrumentType={instrument_type}"
1219
+ + f"&region={region}&delay={str(delay)}&universe={universe}{type_param}&dataset.id={dataset_id}&limit=50"
1220
+ + "&offset={x}"
1221
+ )
1222
+ count = s.get(url_template.format(x=0)).json()["count"]
1223
+ if count == 0:
1224
+ logger.warning(
1225
+ f"No fields found: region={region}, delay={str(delay)}, universe={universe}, "
1226
+ f"type={data_type}, dataset.id={dataset_id}"
1227
+ )
1228
+ return pd.DataFrame()
1229
+
1230
+ else:
1231
+ url_template = (
1232
+ brain_api_url
1233
+ + "/data-fields?"
1234
+ + f"&instrumentType={instrument_type}"
1235
+ + f"&region={region}&delay={str(delay)}&universe={universe}{type_param}&limit=50"
1236
+ + f"&search={search}"
1237
+ + "&offset={x}"
1238
+ )
1239
+ count = 100
1240
+
1241
+ max_try = 5
1242
+ datafields_list = []
1243
+ for x in range(0, count, 50):
1244
+ for _ in range(max_try):
1245
+ datafields = s.get(url_template.format(x=x))
1246
+ while datafields.status_code == 429:
1247
+ print("status_code 429, sleep 3 seconds")
1248
+ time.sleep(3)
1249
+ datafields = s.get(url_template.format(x=x))
1250
+ if "results" in datafields.json():
1251
+ break
1252
+ time.sleep(5)
1253
+
1254
+ datafields_list.append(datafields.json()["results"])
1255
+
1256
+ datafields_list_flat = [item for sublist in datafields_list for item in sublist]
1257
+
1258
+ datafields_df = pd.DataFrame(datafields_list_flat)
1259
+ datafields_df = expand_dict_columns(datafields_df)
1260
+ return datafields_df
1261
+
1262
+
1263
+ def get_operators(s: SingleSession) -> pd.DataFrame:
1264
+ """
1265
+ Fetches and processes the list of operators from the WorldQuant Brain API.
1266
+
1267
+ This function retrieves the operators from the provided session `s`,
1268
+ explodes the 'scope' column (which contains lists) into separate rows,
1269
+ and returns the resulting DataFrame.
1270
+
1271
+ Args:
1272
+ s (SingleSession): An authenticated session object.
1273
+
1274
+ Returns:
1275
+ pd.DataFrame: A DataFrame containing the operators with each scope entry
1276
+ as a separate row.
1277
+ """
1278
+ df = pd.DataFrame(s.get(brain_api_url + "/operators").json())
1279
+ return df.explode('scope').reset_index(drop=True)
1280
+
1281
+
1282
+ def get_instrument_type_region_delay(s: SingleSession) -> pd.DataFrame:
1283
+ """
1284
+ Retrieves and organizes instrument type, region, and delay data into a DataFrame.
1285
+
1286
+ Parameters:
1287
+ s (SingleSession): The session object used for making the API call.
1288
+
1289
+ Returns:
1290
+ df (pd.DataFrame): A DataFrame containing the instrument type, region, delay, universe, and neutralization data.
1291
+
1292
+ The function fetches the settings options from the simulations endpoint and extracts the 'Instrument type',
1293
+ 'Region', 'Universe', 'Delay', and 'Neutralization' data. It then organizes this data into a list of dictionaries,
1294
+ each containing the instrument type, region, delay, universe, and neutralization for a particular combination
1295
+ of instrument type, region, and delay. This list is then converted into a DataFrame and returned.
1296
+ """
1297
+
1298
+ settings_options = s.options(brain_api_url + "/simulations").json()["actions"]["POST"]["settings"]["children"]
1299
+ data = [
1300
+ {settings_options[key]['label']: settings_options[key]['choices']}
1301
+ for key in settings_options.keys()
1302
+ if settings_options[key]['type'] == 'choice'
1303
+ ]
1304
+
1305
+ instrument_type_data = {}
1306
+ region_data = {}
1307
+ universe_data = {}
1308
+ delay_data = {}
1309
+ neutralization_data = {}
1310
+
1311
+ for item in data:
1312
+ if 'Instrument type' in item:
1313
+ instrument_type_data = item['Instrument type']
1314
+ elif 'Region' in item:
1315
+ region_data = item['Region']['instrumentType']
1316
+ elif 'Universe' in item:
1317
+ universe_data = item['Universe']['instrumentType']
1318
+ elif 'Delay' in item:
1319
+ delay_data = item['Delay']['instrumentType']
1320
+ elif 'Neutralization' in item:
1321
+ neutralization_data = item['Neutralization']['instrumentType']
1322
+
1323
+ data_list = []
1324
+
1325
+ for instrument_type in instrument_type_data:
1326
+ for region in region_data[instrument_type['value']]:
1327
+ for delay in delay_data[instrument_type['value']]['region'][region['value']]:
1328
+ row = {'InstrumentType': instrument_type['value'], 'Region': region['value'], 'Delay': delay['value']}
1329
+ row['Universe'] = [
1330
+ item['value'] for item in universe_data[instrument_type['value']]['region'][region['value']]
1331
+ ]
1332
+ row['Neutralization'] = [
1333
+ item['value'] for item in neutralization_data[instrument_type['value']]['region'][region['value']]
1334
+ ]
1335
+ data_list.append(row)
1336
+
1337
+ df = pd.DataFrame(data_list).sort_values(
1338
+ by=['InstrumentType', 'Region', 'Delay'], ascending=False, ignore_index=True
1339
+ )
1340
+ return df
1341
+
1342
+
1343
+ def performance_comparison(
1344
+ s: SingleSession, alpha_id: str, team_id: Optional[str] = None, competition: Optional[str] = None
1345
+ ) -> dict:
1346
+ """
1347
+ Retrieve performance comparison data for merged performance check.
1348
+
1349
+ Args:
1350
+ s (SingleSession): An authenticated session object.
1351
+ alpha_id (str): The ID of the alpha.
1352
+ team_id (str, optional): The ID of the team for comparison. Defaults to None.
1353
+ competition (str, optional): The ID of the competition for comparison. Defaults to None.
1354
+
1355
+ Returns:
1356
+ dict: A dictionary containing the performance comparison data.
1357
+
1358
+ Raises:
1359
+ requests.exceptions.RequestException: If there's an error in the API request.
1360
+ """
1361
+ if competition is not None:
1362
+ part_url = f"competitions/{competition}"
1363
+ elif team_id is not None:
1364
+ part_url = f"teams/{team_id}"
1365
+ else:
1366
+ part_url = "users/self"
1367
+ while True:
1368
+ result = s.get(brain_api_url + f"/{part_url}/alphas/" + alpha_id + "/before-and-after-performance")
1369
+ if "retry-after" in result.headers:
1370
+ time.sleep(float(result.headers["Retry-After"]))
1371
+ else:
1372
+ break
1373
+ if result.json().get("stats", 0) == 0:
1374
+ logger.warning(f"Cant get performance comparison for alpha_id {alpha_id}. {result.json()}")
1375
+ return {}
1376
+ if result.status_code != 200:
1377
+ logger.warning(f"Cant get performance comparison for alpha_id {alpha_id}. {result.json()}")
1378
+ return {}
1379
+
1380
+ return result.json()
1381
+
1382
+
1383
+ def construct_selection_expression(
1384
+ selection: str,
1385
+ instrument_type: Literal["EQUITY"] = "EQUITY",
1386
+ region: str = "USA",
1387
+ delay: Literal[0, 1] = 1,
1388
+ selection_limit: int = 1000,
1389
+ selection_handling: str = "POSITIVE",
1390
+ ) -> dict:
1391
+ """
1392
+ Construct a dictionary containing parameters for a selection expression.
1393
+
1394
+ This function creates a dictionary with the necessary parameters to define
1395
+ a selection expression for use in super alpha simulations.
1396
+
1397
+ Args:
1398
+ selection (str): The selection expression to be used.
1399
+ instrument_type (str, optional): Instrument type to use.
1400
+ Defaults to "EQUITY".
1401
+ region (str, optional): The geographic region for the selection.
1402
+ Defaults to "USA".
1403
+ delay (int, optional): The delay parameter for the selection.
1404
+ Defaults to 1.
1405
+ selection_limit (int, optional): The maximum number of instruments
1406
+ to be selected. Defaults to 1000.
1407
+ selection_handling (str, optional): The method for handling the
1408
+ selection. Defaults to "POSITIVE".
1409
+
1410
+ Returns:
1411
+ dict: A dictionary containing the constructed selection expression
1412
+ parameters, ready to be used in API calls or other functions.
1413
+ """
1414
+ selection_data = {
1415
+ "settings.instrumentType": instrument_type,
1416
+ "settings.region": region,
1417
+ "settings.delay": delay,
1418
+ "selection": selection,
1419
+ "limit": 10,
1420
+ "selectionLimit": selection_limit,
1421
+ "selectionHandling": selection_handling,
1422
+ }
1423
+ return selection_data
1424
+
1425
+
1426
+ def run_selection(s: SingleSession, selection_data: dict) -> dict:
1427
+ """
1428
+ Run a selection simulation using the provided selection data.
1429
+
1430
+ Args:
1431
+ s (SingleSession): An authenticated session object.
1432
+ selection_data (dict): A dictionary containing the selection parameters.
1433
+
1434
+ Returns:
1435
+ dict: A dictionary containing the count of selected alphas and any messages.
1436
+
1437
+ Raises:
1438
+ requests.exceptions.RequestException: If there's an error in the API request.
1439
+ """
1440
+ selection_response = s.get(brain_api_url + "/simulations/super-selection", params=selection_data)
1441
+ r = selection_response.json()
1442
+ selected_alphas_count = r.get("count")
1443
+ message = r.get("message", "")
1444
+ time.sleep(2)
1445
+ return {"selected_alphas_count": selected_alphas_count, "message": message}
1446
+
1447
+
1448
+ def get_alpha_daily_pnl(s: SingleSession, alpha_id: str) -> pd.DataFrame:
1449
+ """
1450
+ Retrieve the daily PnL data for a specific alpha.
1451
+
1452
+ Args:
1453
+ s (SingleSession): An authenticated session object.
1454
+ alpha_id (str): The ID of the alpha.
1455
+
1456
+ Returns:
1457
+ pandas.DataFrame: A DataFrame containing the PnL data for the alpha.
1458
+ """
1459
+
1460
+ return _get_alpha_pnl(s, alpha_id, "daily-pnl")
1461
+
1462
+
1463
+ def submit_alpha(s: SingleSession, alpha_id: str) -> bool:
1464
+ """
1465
+ Submit an alpha for evaluation.
1466
+
1467
+ Args:
1468
+ s (SingleSession): An authenticated session object.
1469
+ alpha_id (str): The ID of the alpha to submit.
1470
+
1471
+ Returns:
1472
+ bool: True if the submission was successful, False otherwise.
1473
+
1474
+ Raises:
1475
+ requests.exceptions.RequestException: If there's an error in the API request.
1476
+ """
1477
+ result = s.post(brain_api_url + "/alphas/" + alpha_id + "/submit")
1478
+ while True:
1479
+ if "retry-after" in result.headers:
1480
+ time.sleep(float(result.headers["Retry-After"]))
1481
+ result = s.get(brain_api_url + "/alphas/" + alpha_id + "/submit")
1482
+ else:
1483
+ break
1484
+ return result.status_code == 200
1485
+
1486
+
1487
+
1488
+ def main():
1489
+ """
1490
+ Main function to demonstrate the usage of the library.
1491
+
1492
+ This function creates a session, generates a list of sample alphas,
1493
+ and simulates them using the simulate_alpha_list function.
1494
+ """
1495
+
1496
+ s = start_session()
1497
+
1498
+ k = [
1499
+ "vwap * 2",
1500
+ "open * close",
1501
+ "high * low",
1502
+ "vwap * 3",
1503
+ "open * close",
1504
+ "high * low",
1505
+ ]
1506
+ alpha_list = [generate_alpha(x) for x in k]
1507
+
1508
+ simulate_alpha_list(s, alpha_list)
1509
+
1510
+
1511
+ if __name__ == "__main__":
1512
+ main()