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.
- zipline_polygon_bundle/__init__.py +22 -0
- zipline_polygon_bundle/adjustments.py +151 -0
- zipline_polygon_bundle/bundle.py +543 -0
- zipline_polygon_bundle/concat_all_aggs.py +246 -0
- zipline_polygon_bundle/concat_all_aggs_partitioned.py +173 -0
- zipline_polygon_bundle/config.py +113 -0
- zipline_polygon_bundle/polygon_file_reader.py +104 -0
- zipline_polygon_bundle/process_all_aggs.py +100 -0
- zipline_polygon_bundle/split_aggs_by_ticker.py +63 -0
- zipline_polygon_bundle/tickers_and_names.py +422 -0
- zipline_polygon_bundle-0.1.6.dist-info/LICENSE +661 -0
- zipline_polygon_bundle-0.1.6.dist-info/METADATA +797 -0
- zipline_polygon_bundle-0.1.6.dist-info/RECORD +14 -0
- zipline_polygon_bundle-0.1.6.dist-info/WHEEL +4 -0
@@ -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)
|