-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.py
380 lines (294 loc) · 10.8 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""
Data downloads, etc.
"""
# External imports
import pandas as pd # type hints
import num2words as numwords # idx to words
import mypytoolkit as kit # html title correciton
# Rich
from rich.console import Console; console = Console()
from rich.table import Table
from rich.text import Text
# Local imports
import os # file paths
import config
import time # prevent os error 22
import json # store params as json
# Project modules
import systems
# ---- Constants ----
LABELS = {"train", "up", "down", "chop"}
# ---- Console tools ----
def create_recorded_console() -> Console:
"""
Creates a new console object with recording enabled,
so it can then be exported as HTML etc.
"""
return Console(record=True, width=120, height=25)
# ---- Labeling ----
def full_label_name(label: str) -> str:
"""
Returns the full label name. Ex. "train" becomes "In-Sample Training",
"up" becomes "Out-of-Sample Uptrend", etc.
"""
assert isinstance(label, str)
assert (label := label.lower()) in LABELS
if label == "train": return "In-Sample Training Results"
elif label == "up": return "Out-of-Sample Uptrend Results"
elif label == "down": return "Out-of-Sample Downtrend Results"
elif label == "chop": return "Out-of-Sample Chop Results"
# ---- Files ----
current_dir = os.path.dirname(
os.path.realpath(__file__)
)
def handle_os_error(function):
"""
Decorator to handle OS Error 22.
"""
def wrapper(*args, **kwargs):
try:
function(*args, **kwargs)
except OSError:
time.sleep(0.1)
wrapper(*args, **kwargs)
return wrapper
def plot_path(idx: int, label: str, flag_nonexistent: bool = False) -> str:
"""
Searches for the result path of the given indexed strategy.
If it doesn't exist, returns False.
Label is the plot type, either "train", "up", "down", or "chop".
"""
if label not in LABELS:
raise ValueError(f"Invalid plot label value, {label}.")
name = idx_to_name(idx, lower=True)
path = os.path.join(current_dir, "results", "plots", f"{name} {label}.html")
if flag_nonexistent:
if not os.path.exists(path): return ""
return path
def stats_path(idx: int, flag_nonexistent: bool = False) -> str:
"""
Searches for the result path of the given indexed strategy.
If it doesn't exist, returns False.
"""
name = idx_to_name(idx, lower=True)
path = os.path.join(current_dir, "results", "stats", f"{name}.html")
if flag_nonexistent:
if not os.path.exists(path): return ""
return path
def results_path(idx: int, label: str) -> str:
"""
Gets a results path.
"""
assert isinstance(label, str)
label = label.lower()
assert label in LABELS
name = idx_to_name(idx, lower=True)
return os.path.join(current_dir, "results", "raw", f"{name} {label}.csv")
def system_exists(index: int) -> bool:
"""
Check if a system by index.
"""
try:
index = int(index)
except ValueError:
raise ValueError("Index must be an integer.")
return index in systems.systems
@handle_os_error
def correct_html_title(name: str, filepath: str, insert: bool = False) -> None:
"""
Opens the HTML file and replaces the <title> field
with the provided name. Recurses if OSError is raised,
sleeping 0.1 seconds until it successfully fixes the file.
If insert is true, inserts a new title tag after the <meta> tag,
assuming there is no title tag in the document yet.
"""
if insert:
kit.append_by_query(
query = "<meta", # no close brace
content = f"\t\t<title>{name}</title>",
file = fr"{filepath}",
replace = False
)
else:
kit.append_by_query(
query = "<title>",
content = f"\t\t<title>{name}</title>",
file = fr"{filepath}",
replace = True
)
@handle_os_error
def insert_html_favicon(filepath: str) -> None:
"""
Inserts favicon.
"""
kit.append_by_query(
query = "<meta",
content = '\t\t<link rel="icon" href="favicon.PNG">',
file = fr"{filepath}"
)
def store_params(system_name: str, params: dict) -> None:
"""
Takes dictionary parameters (usually optimized) and stores them in the
results/optimizers subdirectory, with a system name file name in JSON format.
"""
system_name = system_name.lower()
param_path = os.path.join(
current_dir,
"results",
"optimizers",
f"{system_name}.json"
)
# Convert all data types to floats (np unrecognizable)
params = {param: float(value) for param, value in params.items()}
# Write the file
with open(param_path, "w") as param_file:
param_file.write(json.dumps(params, indent=4))
def read_params(system_name: str = None, system_idx: int = None) -> dict[str, float]:
"""
Reads stored parameters. Provide either `system_name` or `system_idx`. If both
are provided for some reason, idx takes precendence. At least one must be provided.
If the queried parameters json file does not exist, returns an empty dictionary.
"""
if not system_name and not system_idx:
raise ValueError("You must provide either a system name or index.")
if system_name: system_name = system_name.lower()
elif system_idx: system_name = idx_to_name(system_idx, lower=True)
param_path = os.path.join(
current_dir,
"results",
"optimizers",
f"{system_name}.json"
)
# Ensure file exists
if not os.path.exists(param_path): return {}
with open(param_path, 'r') as param_file:
return json.load(param_file)
# ---- Language ----
def idx_to_name(
idx: int,
prefix: str = "Wooster ",
title: bool = True,
lower: bool = False
) -> str:
"""
Ex. turns `2` into "Wooster Two", and 23 into "Wooster TwentyTwo".
`lower` supercedes title.
"""
num = str(numwords.num2words(idx))
words = [word.title() if title else word for word in num.split("-")]
# Connect the words
full_num = "".join(words)
# Deal with spaces and "and" if > 100
full_num = "".join([word for word in full_num.split(" ") if word.lower() != "and"])
_res = prefix + full_num
return _res.lower() if lower else _res
# ---- Results ----
def store_results(idx: int, results: dict[str, pd.Series]) -> None:
"""
Takes in fully computed dict results with labels and stores them as individual
CSV files.
"""
for label, result in results.items():
path = results_path(idx, label)
# Remove private, complex items like trades df and equity curve
for metric, value in result.copy().items():
if metric[0] == "_": result.drop(metric, inplace=True)
if isinstance(value, float): result[metric] = round(value, 3)
result.to_csv(path)
def load_results(idx: int) -> dict[str, pd.Series]:
"""
Loads previously stored results into format readable by display results.
"""
results_dir = os.path.join(current_dir, "results", "raw")
files = os.listdir(results_dir)
results = {}
for label in LABELS:
expected_file = results_path(idx, label)
if os.path.basename(expected_file) not in files:
continue
# Read with squeeze to allow returning a series with one column
results[label] = pd.read_csv(expected_file, index_col=0).squeeze("columns")
return results
def _render_results(results: pd.Series) -> Table:
"""
Renders results to console. Provide a results series. Only need to provide either
`idx` or `name`.
"""
# Get all metrics as long as they're not private attributes
all_metrics = [metric for metric in results.index if metric[0] != "_"]
# Preferred metrics
preferred_table = Table(
title = Text("Preferred Performance Metrics"),
style = "dim"
)
preferred_table.add_column("Metric")
preferred_table.add_column("Value")
for metric in config.Results.preferred_metrics:
all_metrics.remove(metric)
if isinstance((result := results[metric]), float): result = round(result, 3)
# Style certain results
style = "none"
if metric in config.Results.highlight_preferred:
style = config.Results.highlight_preferred_style
preferred_table.add_row(Text(metric, style), Text(str(result), style))
# Secondary metrics, loop over remaining metrics
secondary_table = Table(
title = Text(
"Secondary Performance Metrics",
style = config.Results.secondary_metrics_style
),
style = config.Results.secondary_metrics_style
)
secondary_table.add_column(
Text(
"Metric",
style=config.Results.secondary_metrics_style
)
)
secondary_table.add_column(
Text(
"Value",
style=config.Results.secondary_metrics_style
)
)
for metric in all_metrics:
if isinstance((result := results[metric]), float): result = round(result, 3)
secondary_table.add_row(
Text(
metric,
style=config.Results.secondary_metrics_style
),
Text(str(result), style=config.Results.secondary_metrics_style)
)
# Display
display_table = Table.grid(padding=0, expand=False)
display_table.add_column(""); display_table.add_column("")
display_table.add_row(preferred_table, secondary_table)
return display_table
def display_results(
results: dict[str, pd.Series],
idx: int,
record: bool = True
) -> None:
"""
Print rendered results to console and save them to console.
"""
if not isinstance(results, dict):
raise ValueError("Results must be a dict with label keys and Series values.")
html_console = create_recorded_console()
if not record: html_console = Console()
for label, result in results.items():
table = _render_results(result)
html_console.line()
html_console.rule(f"[red]{idx_to_name(idx)}[/] {full_label_name(label)}")
html_console.line()
html_console.print(table, justify="center")
html_console.line()
if record:
html_console.save_html(filepath := stats_path(idx))
correct_html_title(
name = f"{idx_to_name(idx)} Performance Metrics",
filepath = filepath,
insert = True
)
insert_html_favicon(filepath)