zipline_polygon_bundle 0.1.6__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.
@@ -0,0 +1,100 @@
1
+ from .config import PolygonConfig
2
+
3
+ import os
4
+ import glob
5
+ import pandas as pd
6
+ import fastparquet as fp
7
+ from pathlib import Path
8
+
9
+
10
+ def apply_to_aggs(
11
+ aggs_path: Path,
12
+ func: callable,
13
+ config: PolygonConfig,
14
+ start_timestamp: pd.Timestamp = None,
15
+ end_timestamp: pd.Timestamp = None,
16
+ ):
17
+ aggs_pf = fp.ParquetFile(aggs_path)
18
+ df = aggs_pf.to_pandas()
19
+ # Drop rows with window_start not in the range config.start_session to config.end_session
20
+ # Those are dates (i.e. time 00:00:00) and end_session is inclusive.
21
+ if start_timestamp is None and end_timestamp is None:
22
+ return func(df, aggs_path, config)
23
+ if start_timestamp is None:
24
+ start_timestamp = pd.Timestamp.min
25
+ if end_timestamp is None:
26
+ end_timestamp = pd.Timestamp.max
27
+ else:
28
+ end_timestamp = end_timestamp + pd.Timedelta(days=1)
29
+ df = df[
30
+ df["window_start"].between(start_timestamp, end_timestamp, inclusive="left")
31
+ ]
32
+ return func(df, aggs_path, config)
33
+
34
+
35
+ # TODO: Return iterable of results
36
+ def apply_to_all_aggs(
37
+ aggs_dir: str,
38
+ func: callable,
39
+ config: PolygonConfig,
40
+ start_timestamp: pd.Timestamp = None,
41
+ end_timestamp: pd.Timestamp = None,
42
+ aggs_pattern: str = "**/*.parquet",
43
+ max_workers=None,
44
+ ) -> list:
45
+ """zipline does bundle ingestion one ticker at a time."""
46
+ paths = list(
47
+ glob.glob(os.path.join(aggs_dir, aggs_pattern), recursive="**" in aggs_pattern)
48
+ )
49
+ print(f"{len(paths)=}")
50
+ print(f"{paths[:5]=}")
51
+ if max_workers == 0:
52
+ return [
53
+ apply_to_aggs(
54
+ path, func, config=config, start_timestamp=start_timestamp, end_timestamp=end_timestamp
55
+ )
56
+ for path in paths
57
+ ]
58
+ else:
59
+ # with ProcessPoolExecutor(max_workers=max_workers) as executor:
60
+ # executor.map(
61
+ # aggs_max_ticker_len,
62
+ # paths,
63
+ # )
64
+ print("Not implemented")
65
+ return None
66
+
67
+
68
+ if __name__ == "__main__":
69
+ # os.environ["POLYGON_DATA_DIR"] = "/Volumes/Oahu/Mirror/files.polygon.io"
70
+ def max_ticker_len(df: pd.DataFrame, path: Path, config: PolygonConfig):
71
+ print(f"{path=}")
72
+ return 0 if df.empty else df["ticker"].str.len().max()
73
+
74
+ config = PolygonConfig(
75
+ environ=os.environ,
76
+ calendar_name="XNYS",
77
+ start_session="2020-10-07",
78
+ end_session="2020-10-15",
79
+ )
80
+ print(f"{config.aggs_dir=}")
81
+ max_ticker_lens = apply_to_all_aggs(
82
+ config.aggs_dir,
83
+ func=max_ticker_len,
84
+ config=config,
85
+ aggs_pattern="2020/10/**/*.parquet",
86
+ start_timestamp=config.start_timestamp,
87
+ end_timestamp=config.end_timestamp,
88
+ max_workers=0,
89
+ )
90
+ print(f"{max_ticker_lens=}")
91
+ print(f"{len(max_ticker_lens)=}")
92
+ print(f"{max(max_ticker_lens)=}")
93
+
94
+ # 2016 to 2024
95
+ # len(max_ticker_lens)=2184
96
+ # max(max_ticker_lens)=10
97
+ # 2020-10-07 to 2020-10-15
98
+ # max_ticker_lens=[0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
99
+ # len(max_ticker_lens)=22
100
+ # max(max_ticker_lens)=8
@@ -0,0 +1,63 @@
1
+ from .config import PolygonConfig
2
+
3
+ import os
4
+ import glob
5
+ import pandas as pd
6
+ from concurrent.futures import ProcessPoolExecutor
7
+ import fastparquet as fp
8
+ from pathlib import Path
9
+
10
+
11
+ def split_aggs_by_ticker(aggs_path: Path, by_ticker_dir: Path, extension: str):
12
+ aggs_pf = fp.ParquetFile(aggs_path)
13
+ df = aggs_pf.to_pandas()
14
+ print(df.info())
15
+ print(df.head())
16
+ df.sort_values(["ticker", "window_start"], inplace=True)
17
+ for ticker in df["ticker"].unique():
18
+ ticker_df = df[df["ticker"] == ticker]
19
+ ticker_path = by_ticker_dir / f"{ticker}{extension}"
20
+ if os.path.exists(ticker_path):
21
+ fp.write(ticker_path, ticker_df, has_nulls=False, write_index=False, fixed_text={"ticker": len(ticker) + 1})
22
+ else:
23
+ fp.write(ticker_path, ticker_df, append=True, has_nulls=False, write_index=False)
24
+
25
+
26
+ def split_all_aggs_by_ticker(
27
+ aggs_dir,
28
+ by_ticker_dir,
29
+ recursive=True,
30
+ extension=".parquet",
31
+ max_workers=None,
32
+ ):
33
+ """zipline does bundle ingestion one ticker at a time."""
34
+ paths = list(
35
+ glob.glob(os.path.join(by_ticker_dir, f"*{extension}"))
36
+ )
37
+ for path in paths:
38
+ if os.path.exists(path):
39
+ print(f"Removing {path}")
40
+ os.remove(path)
41
+ aggs_pattern = f"**/*{extension}" if recursive else f"*{extension}"
42
+ paths = list(
43
+ glob.glob(os.path.join(aggs_dir, aggs_pattern), recursive=recursive)
44
+ )
45
+ if max_workers == 0:
46
+ for path in paths:
47
+ split_aggs_by_ticker(
48
+ path, by_ticker_dir=by_ticker_dir, extension=extension
49
+ )
50
+ else:
51
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
52
+ executor.map(
53
+ split_aggs_by_ticker,
54
+ paths,
55
+ [by_ticker_dir] * len(paths),
56
+ [extension] * len(paths),
57
+ )
58
+
59
+
60
+ if __name__ == "__main__":
61
+ os.environ["POLYGON_DATA_DIR"] = "/Volumes/Oahu/Mirror/files.polygon.io"
62
+ config = PolygonConfig(environ=os.environ, calendar_name="XNYS", start_session=None, end_session=None)
63
+ split_all_aggs_by_ticker(config.minute_aggs_dir)
@@ -0,0 +1,422 @@
1
+ from .config import PolygonConfig
2
+
3
+ import datetime
4
+ import os
5
+ import pandas as pd
6
+ import polygon
7
+ import logging
8
+ from concurrent.futures import ProcessPoolExecutor
9
+
10
+
11
+ def configure_logging():
12
+ logging.basicConfig(
13
+ level=logging.WARNING,
14
+ format="%(asctime)s %(processName)s %(levelname)s %(message)s",
15
+ handlers=[logging.StreamHandler()],
16
+ )
17
+
18
+
19
+ def fetch_and_save_tickers_for_date(config: PolygonConfig, date: pd.Timestamp):
20
+ try:
21
+ configure_logging()
22
+ logger = logging.getLogger()
23
+ logger.info(f"Fetching tickers for {date}")
24
+ assets = PolygonAssets(config)
25
+ all_tickers = assets.fetch_all_tickers(date=date)
26
+ assets.save_tickers_for_date(all_tickers, date)
27
+ except Exception as e:
28
+ logger.exception(f"Error fetching tickers for {date}: {e}")
29
+
30
+
31
+ def unique_simple_list(x):
32
+ return [str(y) for y in x.unique() if pd.notnull(y)]
33
+
34
+
35
+ def unique_simple_date_list(x):
36
+ return [y.date().isoformat() for y in x.unique() if pd.notnull(y)]
37
+
38
+
39
+ class PolygonAssets:
40
+ def __init__(self, config: PolygonConfig):
41
+ self.config = config
42
+ self.polygon_client = polygon.RESTClient(api_key=config.api_key)
43
+
44
+ def fetch_tickers(
45
+ self,
46
+ date: pd.Timestamp,
47
+ active: bool = True,
48
+ ):
49
+ response = self.polygon_client.list_tickers(
50
+ market=self.config.market, active=active, date=date.date(), limit=500
51
+ )
52
+ tickers_df = pd.DataFrame(list(response))
53
+ # The currency info is for crypto. The source_feed is always NA.
54
+ tickers_df.drop(
55
+ columns=[
56
+ "currency_symbol",
57
+ "base_currency_symbol",
58
+ "base_currency_name",
59
+ "source_feed",
60
+ ],
61
+ inplace=True,
62
+ )
63
+
64
+ tickers_df["request_date"] = date
65
+ tickers_df["last_updated_utc"] = pd.to_datetime(tickers_df["last_updated_utc"])
66
+ tickers_df["delisted_utc"] = pd.to_datetime(tickers_df["delisted_utc"])
67
+
68
+ # Make sure there are no leading or trailing spaces in the column values
69
+ for col in tickers_df.columns:
70
+ if tickers_df[col].dtype == "string":
71
+ # Still gotta check value types because of NA values.
72
+ tickers_df[col] = tickers_df[col].apply(
73
+ lambda x: x.strip() if isinstance(x, str) else x
74
+ )
75
+
76
+ # Test tickers have no CIK value.
77
+ # Actually sometimes listed tickers have no CIK value.
78
+ # Case in point is ALTL ETF which doesn't include a CIK in the Ticker search response for 2024-07-01
79
+ # but did on 2024-06-25. Suspiciouly 4 years after the listing date of 2020-06-25.
80
+ # We'll have to leave this cleaning of test tickers until we can call Ticker Details.
81
+ # tickers_df = tickers_df.dropna(subset=["ticker", "cik"])
82
+
83
+ return tickers_df
84
+
85
+ def validate_active_tickers(self, tickers: pd.DataFrame):
86
+ # # All tickers are active
87
+ # inactive_tickers = (
88
+ # tickers[~tickers.index.get_level_values("active")]["ticker"].unique().tolist()
89
+ # )
90
+ # assert (
91
+ # not inactive_tickers
92
+ # ), f"{len(inactive_tickers)} tickers are not active: {inactive_tickers[:15]}"
93
+
94
+ # No tickers with missing last_updated_utc
95
+ missing_last_updated_utc_tickers = (
96
+ tickers[tickers["last_updated_utc"].isnull()]["ticker"].unique().tolist()
97
+ )
98
+ assert (
99
+ not missing_last_updated_utc_tickers
100
+ ), f"{len(missing_last_updated_utc_tickers)} tickers have missing last_updated_utc: {missing_last_updated_utc_tickers[:15]}"
101
+
102
+ # # No tickers with missing name
103
+ # missing_name_tickers = (
104
+ # tickers[tickers["name"].isnull()]["ticker"].unique().tolist()
105
+ # )
106
+ # if missing_name_tickers:
107
+ # logging.warning(
108
+ # f"{len(missing_name_tickers)} tickers have missing name: {missing_name_tickers[:15]}"
109
+ # )
110
+ # assert (
111
+ # not missing_name_tickers
112
+ # ), f"{len(missing_name_tickers)} tickers have missing name: {missing_name_tickers[:15]}"
113
+
114
+ # No tickers with missing locale
115
+ missing_locale_tickers = (
116
+ tickers[tickers["locale"].isnull()]["ticker"].unique().tolist()
117
+ )
118
+ assert (
119
+ not missing_locale_tickers
120
+ ), f"{len(missing_locale_tickers)} tickers have missing locale: {missing_locale_tickers[:15]}"
121
+
122
+ # No tickers with missing market
123
+ missing_market_tickers = (
124
+ tickers[tickers["market"].isnull()]["ticker"].unique().tolist()
125
+ )
126
+ assert (
127
+ not missing_market_tickers
128
+ ), f"{len(missing_market_tickers)} tickers have missing market: {missing_market_tickers[:15]}"
129
+
130
+ # # No tickers with missing primary exchange
131
+ # missing_primary_exchange_tickers = (
132
+ # tickers[tickers["primary_exchange"].isnull()]
133
+ # .index.get_level_values("ticker")
134
+ # .tolist()
135
+ # )
136
+ # # We'll just warn here and filter in the merge_tickers function.
137
+ # if missing_primary_exchange_tickers:
138
+ # logging.warning(
139
+ # f"{len(missing_primary_exchange_tickers)} tickers have missing primary exchange: {missing_primary_exchange_tickers[:15]}"
140
+ # )
141
+ # assert (
142
+ # not missing_primary_exchange_tickers
143
+ # ), f"{len(missing_primary_exchange_tickers)} tickers have missing primary exchange: {missing_primary_exchange_tickers[:15]}"
144
+
145
+ # # No tickers with missing type
146
+ # missing_type_tickers = (
147
+ # tickers[tickers["type"].isnull()].index.get_level_values("ticker").tolist()
148
+ # )
149
+ # # assert not missing_type_tickers, f"{len(missing_type_tickers)} tickers have missing type: {missing_type_tickers[:15]}"
150
+ # # Just a warning because there are legit tickers with missing type
151
+ # if missing_type_tickers:
152
+ # logging.warning(
153
+ # f"{len(missing_type_tickers)} tickers have missing type: {missing_type_tickers[:15]}"
154
+ # )
155
+
156
+ # No tickers with missing currency
157
+ missing_currency_tickers = (
158
+ tickers[tickers["currency_name"].isnull()]["ticker"].unique().tolist()
159
+ )
160
+ assert (
161
+ not missing_currency_tickers
162
+ ), f"{len(missing_currency_tickers)} tickers have missing currency_name: {missing_currency_tickers[:15]}"
163
+
164
+ def fetch_all_tickers(self, date: pd.Timestamp):
165
+ all_tickers = pd.concat(
166
+ [
167
+ self.fetch_tickers(date=date, active=True),
168
+ self.fetch_tickers(date=date, active=False),
169
+ ]
170
+ )
171
+
172
+ # We're keeping these tickers with missing type because it is some Polygon bug.
173
+ # # Drop rows with no type. Not sure why this happens but we'll ignore them for now.
174
+ # active_tickers = active_tickers.dropna(subset=["type"])
175
+
176
+ self.validate_active_tickers(all_tickers)
177
+
178
+ return all_tickers
179
+
180
+ def ticker_file_exists(self, date: pd.Timestamp):
181
+ return os.path.exists(self.config.ticker_file_path(date))
182
+
183
+ def save_tickers_for_date(self, tickers: pd.DataFrame, date: pd.Timestamp):
184
+ tickers.to_parquet(self.config.ticker_file_path(date))
185
+
186
+ def load_tickers_for_date(self, date: pd.Timestamp):
187
+ try:
188
+ tickers = pd.read_parquet(self.config.ticker_file_path(date))
189
+ return tickers
190
+ except (FileNotFoundError, IOError) as e:
191
+ logging.error(f"Error loading tickers for {date}: {e}")
192
+ try:
193
+ # Remove the file so that it can be fetched again
194
+ os.remove(self.config.ticker_file_path(date))
195
+ except (FileNotFoundError, IOError) as e2:
196
+ logging.error(
197
+ f"Error removing file {self.config.ticker_file_path(date)}: {e2}"
198
+ )
199
+ return None
200
+
201
+ def load_all_tickers(self, fetch_missing=False, max_workers=None):
202
+ dates = list(
203
+ self.config.calendar.trading_index(
204
+ start=self.config.start_timestamp,
205
+ end=self.config.end_timestamp,
206
+ period="1D",
207
+ )
208
+ )
209
+ if fetch_missing:
210
+ missing_dates = [
211
+ date for date in dates if not self.ticker_file_exists(date)
212
+ ]
213
+ print(f"{len(missing_dates)=}")
214
+ if missing_dates:
215
+ max_workers = (
216
+ max_workers if max_workers is not None else self.config.max_workers
217
+ )
218
+ if max_workers == 0:
219
+ for date in missing_dates:
220
+ fetch_and_save_tickers_for_date(self.config, date)
221
+ else:
222
+ print("Starting process pool executor")
223
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
224
+ executor.map(
225
+ fetch_and_save_tickers_for_date,
226
+ [self.config] * len(missing_dates),
227
+ missing_dates,
228
+ )
229
+ dates_with_files = [date for date in dates if self.ticker_file_exists(date)]
230
+ if fetch_missing and (len(dates_with_files) != len(dates)):
231
+ logging.warning(
232
+ f"Only {len(dates_with_files)} of {len(dates)} dates have files."
233
+ )
234
+ all_tickers = pd.concat(
235
+ [self.load_tickers_for_date(date) for date in dates_with_files]
236
+ )
237
+ return all_tickers
238
+
239
+ def merge_tickers(self, all_tickers: pd.DataFrame):
240
+ all_tickers.set_index(
241
+ ["ticker", "primary_exchange", "cik", "type", "composite_figi", "active"],
242
+ drop=False,
243
+ inplace=True,
244
+ )
245
+ all_tickers.sort_index(inplace=True)
246
+
247
+ # Make sure there are no leading or trailing spaces in the column values
248
+ # This also does some kind of change to the type for the StringArray values which makes Arrow Parquet happy.
249
+ all_tickers = all_tickers.map(lambda x: x.strip() if isinstance(x, str) else x)
250
+
251
+ # # Drops rows with missing primary exchange (rare but means it isn't actually active).
252
+ # all_tickers.dropna(subset=["name", "primary_exchange"], inplace=True)
253
+
254
+ # # Only keep rows with type CS, ETF, or ADRC
255
+ # all_tickers = all_tickers[all_tickers["type"].isin(["CS", "ETF", "ADRC"])]
256
+
257
+ merged_tickers = (
258
+ all_tickers.groupby(
259
+ level=[
260
+ "ticker",
261
+ "primary_exchange",
262
+ "cik",
263
+ "type",
264
+ "composite_figi",
265
+ "active",
266
+ ],
267
+ dropna=False,
268
+ )
269
+ .agg(
270
+ {
271
+ "request_date": ["min", "max"],
272
+ "last_updated_utc": lambda x: x.max().date(),
273
+ "name": "unique",
274
+ "share_class_figi": unique_simple_list,
275
+ "delisted_utc": unique_simple_date_list,
276
+ "currency_name": "unique",
277
+ "locale": "unique",
278
+ "market": "unique",
279
+ }
280
+ )
281
+ .sort_values(by=("request_date", "max"))
282
+ )
283
+
284
+ # Flatten the multi-level column index
285
+ merged_tickers.columns = [
286
+ "_".join(col).strip() for col in merged_tickers.columns.values
287
+ ]
288
+
289
+ # Rename the columns
290
+ merged_tickers.rename(
291
+ columns={
292
+ "request_date_min": "start_date",
293
+ "request_date_max": "end_date",
294
+ "last_updated_utc_<lambda>": "last_updated_utc",
295
+ "share_class_figi_unique_simple_list": "share_class_figi",
296
+ "delisted_utc_unique_simple_date_list": "delisted_utc",
297
+ },
298
+ inplace=True,
299
+ )
300
+ merged_tickers.rename(
301
+ columns=lambda x: x.removesuffix("_unique"),
302
+ inplace=True,
303
+ )
304
+
305
+ all_tickers.sort_index(inplace=True)
306
+ return merged_tickers
307
+
308
+
309
+ def list_to_string(x):
310
+ if not hasattr(x, "__len__"):
311
+ return str(x)
312
+ if len(x) == 0:
313
+ return ""
314
+ if len(x) == 1:
315
+ return str(x[0])
316
+ s = set([str(y) for y in x])
317
+ return f"[{']['.join(sorted(list(s)))}]"
318
+
319
+
320
+ def get_ticker_universe(config: PolygonConfig, fetch_missing: bool = False):
321
+ tickers_csv_path = config.tickers_csv_path
322
+ print(f"{tickers_csv_path=}")
323
+ parquet_path = tickers_csv_path.removesuffix(".csv") + ".parquet"
324
+ if not os.path.exists(parquet_path):
325
+ if os.path.exists(tickers_csv_path):
326
+ os.remove(tickers_csv_path)
327
+ assets = PolygonAssets(config)
328
+ all_tickers = assets.load_all_tickers(fetch_missing=fetch_missing)
329
+ all_tickers.info()
330
+ # all_tickers.to_csv(tickers_csv_path)
331
+ logging.info("Merging tickers")
332
+ merged_tickers = assets.merge_tickers(all_tickers)
333
+ merged_tickers.info()
334
+ merged_tickers.to_parquet(tickers_csv_path.removesuffix(".csv") + ".parquet")
335
+ print(
336
+ f"Saved {len(merged_tickers)} tickers to {tickers_csv_path.removesuffix('.csv') + '.parquet'}"
337
+ )
338
+ if not os.path.exists(tickers_csv_path):
339
+ merged_tickers = pd.read_parquet(parquet_path)
340
+ merged_tickers["name"] = merged_tickers["name"].apply(list_to_string)
341
+ merged_tickers["share_class_figi"] = merged_tickers["share_class_figi"].apply(
342
+ list_to_string
343
+ )
344
+ merged_tickers["delisted_utc"] = merged_tickers["delisted_utc"].apply(
345
+ list_to_string
346
+ )
347
+ merged_tickers["currency_name"] = merged_tickers["currency_name"].apply(
348
+ list_to_string
349
+ )
350
+ merged_tickers["locale"] = merged_tickers["locale"].apply(list_to_string)
351
+ merged_tickers["market"] = merged_tickers["market"].apply(list_to_string)
352
+ merged_tickers.to_csv(
353
+ tickers_csv_path, escapechar="\\", quoting=csv.QUOTE_NONNUMERIC
354
+ )
355
+ print(f"Saved {len(merged_tickers)} tickers to {tickers_csv_path}")
356
+
357
+ # merged_tickers = pd.read_csv(
358
+ # tickers_csv_path,
359
+ # escapechar="\\",
360
+ # quoting=csv.QUOTE_NONNUMERIC,
361
+ # dtype={
362
+ # "ticker": str,
363
+ # "primary_exchange": str,
364
+ # "cik": str,
365
+ # "type": str,
366
+ # "share_class_figi": str,
367
+ # },
368
+ # # converters={
369
+ # # "ticker": lambda x: str(x),
370
+ # # "start_date": lambda x: pd.to_datetime(x),
371
+ # # "cik": lambda x: str(x) if x else None,
372
+ # # "name": lambda x: str(x),
373
+ # # "end_date": lambda x: pd.to_datetime(x),
374
+ # # "composite_figi": lambda x: str(x).upper(),
375
+ # # "share_class_figi": lambda x: str(x).upper(),
376
+ # # "currency_name": lambda x: str(x).lower(),
377
+ # # "locale": lambda x: str(x).lower(),
378
+ # # "market": lambda x: str(x).lower(),
379
+ # # "primary_exchange": lambda x: str(x).strip().upper(),
380
+ # # "type": lambda x: str(x).upper(),
381
+ # # },
382
+ # )
383
+ merged_tickers = pd.read_parquet(parquet_path)
384
+ merged_tickers.info()
385
+ return merged_tickers
386
+
387
+
388
+ # Initialize ticker files in __main__. Use CLI args to specify start and end dates.
389
+ if __name__ == "__main__":
390
+ import argparse
391
+
392
+ parser = argparse.ArgumentParser(description="Initialize ticker files.")
393
+ parser.add_argument(
394
+ "--start-date",
395
+ type=str,
396
+ help="Start date in ISO format (YYYY-MM-DD)",
397
+ default="2014-05-01",
398
+ )
399
+ parser.add_argument(
400
+ "--end-date",
401
+ type=str,
402
+ help="End date in ISO format (YYYY-MM-DD)",
403
+ default="2024-04-01",
404
+ )
405
+ args = parser.parse_args()
406
+
407
+ start_date = (
408
+ datetime.datetime.strptime(args.start_date, "%Y-%m-%d").date()
409
+ if args.start_date
410
+ else datetime.date.today()
411
+ )
412
+ end_date = (
413
+ datetime.datetime.strptime(args.end_date, "%Y-%m-%d").date()
414
+ if args.end_date
415
+ else datetime.date.today()
416
+ )
417
+
418
+ all_tickers = load_all_tickers(start_date, end_date, fetch_missing=True)
419
+ merged_tickers = merge_tickers(all_tickers)
420
+ merged_tickers.to_csv(f"data/tickers/us_tickers_{start_date}-{end_date}.csv")
421
+ ticker_names = ticker_names_from_merged_tickers(merged_tickers)
422
+ print(ticker_names)