firstrate-data 0.1.0__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.
- firstrate_data/__init__.py +38 -0
- firstrate_data/config.py +49 -0
- firstrate_data/domain/__init__.py +33 -0
- firstrate_data/domain/bar_type.py +34 -0
- firstrate_data/domain/enums.py +120 -0
- firstrate_data/domain/ticker_listing.py +78 -0
- firstrate_data/download/__init__.py +0 -0
- firstrate_data/download/bundles.py +201 -0
- firstrate_data/download/client.py +432 -0
- firstrate_data/download/progress.py +45 -0
- firstrate_data/download/requests.py +245 -0
- firstrate_data/store/__init__.py +0 -0
- firstrate_data/store/_parquet_table.py +78 -0
- firstrate_data/store/_sql.py +399 -0
- firstrate_data/store/store.py +630 -0
- firstrate_data-0.1.0.dist-info/METADATA +449 -0
- firstrate_data-0.1.0.dist-info/RECORD +20 -0
- firstrate_data-0.1.0.dist-info/WHEEL +5 -0
- firstrate_data-0.1.0.dist-info/licenses/LICENSE +201 -0
- firstrate_data-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from firstrate_data.domain import (
|
|
2
|
+
Adjustment,
|
|
3
|
+
AssetType,
|
|
4
|
+
BarType,
|
|
5
|
+
ContinuousFuturesAdjustment,
|
|
6
|
+
ContractFiles,
|
|
7
|
+
DelistedArchive,
|
|
8
|
+
DelistedUpdate,
|
|
9
|
+
EquitiesAdjustment,
|
|
10
|
+
OtherData,
|
|
11
|
+
Period,
|
|
12
|
+
TickerListing,
|
|
13
|
+
Timeframe,
|
|
14
|
+
TradingHours,
|
|
15
|
+
Unadjusted,
|
|
16
|
+
)
|
|
17
|
+
from firstrate_data.download.client import Client
|
|
18
|
+
from firstrate_data.store.store import Ingested, Store
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Adjustment",
|
|
22
|
+
"AssetType",
|
|
23
|
+
"BarType",
|
|
24
|
+
"Client",
|
|
25
|
+
"ContinuousFuturesAdjustment",
|
|
26
|
+
"ContractFiles",
|
|
27
|
+
"DelistedArchive",
|
|
28
|
+
"DelistedUpdate",
|
|
29
|
+
"EquitiesAdjustment",
|
|
30
|
+
"Ingested",
|
|
31
|
+
"OtherData",
|
|
32
|
+
"Period",
|
|
33
|
+
"Store",
|
|
34
|
+
"TickerListing",
|
|
35
|
+
"Timeframe",
|
|
36
|
+
"TradingHours",
|
|
37
|
+
"Unadjusted",
|
|
38
|
+
]
|
firstrate_data/config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from dotenv import find_dotenv, load_dotenv
|
|
5
|
+
|
|
6
|
+
DEFAULT_BASE_URL = "https://firstratedata.com/api"
|
|
7
|
+
_BASE_URL_KEY = "FIRSTRATE_BASE_URL"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MissingSettingError(KeyError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _find_and_load_dotenv_cwd() -> None:
|
|
15
|
+
load_dotenv(find_dotenv(usecwd=True))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _raise_if_missing(env_variable: str) -> str:
|
|
19
|
+
_find_and_load_dotenv_cwd()
|
|
20
|
+
value = os.getenv(env_variable)
|
|
21
|
+
if value:
|
|
22
|
+
return value
|
|
23
|
+
msg = f"set {env_variable} environment variable"
|
|
24
|
+
raise MissingSettingError(msg)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ----------------------------------------------------------------------
|
|
28
|
+
# Store config
|
|
29
|
+
# ----------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def firstrate_data_path() -> Path:
|
|
33
|
+
return Path(_raise_if_missing("FIRSTRATE_DATA_PATH"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------
|
|
37
|
+
# Client config
|
|
38
|
+
# ----------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def firstrate_user_id() -> str:
|
|
42
|
+
return _raise_if_missing("FIRSTRATE_USERID")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def base_url() -> str:
|
|
46
|
+
"""Return the vendor's API root."""
|
|
47
|
+
# FIRSTRATE_BASE_URL lets a test point the client at a stand-in server.
|
|
48
|
+
_find_and_load_dotenv_cwd()
|
|
49
|
+
return os.getenv(_BASE_URL_KEY, DEFAULT_BASE_URL)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from firstrate_data.domain.bar_type import BarType
|
|
2
|
+
from firstrate_data.domain.enums import (
|
|
3
|
+
Adjustment,
|
|
4
|
+
AssetType,
|
|
5
|
+
ContinuousFuturesAdjustment,
|
|
6
|
+
ContractFiles,
|
|
7
|
+
DelistedArchive,
|
|
8
|
+
DelistedUpdate,
|
|
9
|
+
EquitiesAdjustment,
|
|
10
|
+
OtherData,
|
|
11
|
+
Period,
|
|
12
|
+
Timeframe,
|
|
13
|
+
TradingHours,
|
|
14
|
+
Unadjusted,
|
|
15
|
+
)
|
|
16
|
+
from firstrate_data.domain.ticker_listing import TickerListing
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Adjustment",
|
|
20
|
+
"AssetType",
|
|
21
|
+
"BarType",
|
|
22
|
+
"ContinuousFuturesAdjustment",
|
|
23
|
+
"ContractFiles",
|
|
24
|
+
"DelistedArchive",
|
|
25
|
+
"DelistedUpdate",
|
|
26
|
+
"EquitiesAdjustment",
|
|
27
|
+
"OtherData",
|
|
28
|
+
"Period",
|
|
29
|
+
"TickerListing",
|
|
30
|
+
"Timeframe",
|
|
31
|
+
"TradingHours",
|
|
32
|
+
"Unadjusted",
|
|
33
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass, fields, replace
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from firstrate_data.domain.enums import Adjustment, AssetType, Timeframe
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class BarType:
|
|
9
|
+
# declared in the same order used as the store's paths.
|
|
10
|
+
asset_type: AssetType | None = None
|
|
11
|
+
adjustment: Adjustment | None = None
|
|
12
|
+
timeframe: Timeframe | None = None
|
|
13
|
+
ticker: str | None = None
|
|
14
|
+
|
|
15
|
+
def from_ticker(self, ticker: str | None) -> Self:
|
|
16
|
+
"""Return a copy of this `BarType` with `ticker` swapped in."""
|
|
17
|
+
return replace(self, ticker=ticker)
|
|
18
|
+
|
|
19
|
+
def levels(self) -> dict[str, str | None]:
|
|
20
|
+
"""Every level of the tree, in the tree's order. None where unstated."""
|
|
21
|
+
return {
|
|
22
|
+
f.name: None if (value := getattr(self, f.name)) is None else str(value)
|
|
23
|
+
for f in fields(self)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def stated_levels(self) -> dict[str, str]:
|
|
27
|
+
"""The levels this bar type names, in the tree's order."""
|
|
28
|
+
return {
|
|
29
|
+
name: value for name, value in self.levels().items() if value is not None
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def fields(cls) -> list[str]:
|
|
34
|
+
return [f.name for f in fields(cls)]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum, auto
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AssetType(StrEnum):
|
|
8
|
+
STOCK = auto()
|
|
9
|
+
ETF = auto()
|
|
10
|
+
INDEX = auto()
|
|
11
|
+
FUTURES = auto()
|
|
12
|
+
CRYPTO = auto()
|
|
13
|
+
FX = auto()
|
|
14
|
+
OPTIONS = auto()
|
|
15
|
+
|
|
16
|
+
def timezone(self) -> str:
|
|
17
|
+
if self is AssetType.CRYPTO:
|
|
18
|
+
return "UTC"
|
|
19
|
+
return "America/New_York"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# adjustments ----------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Adjustment(StrEnum):
|
|
26
|
+
@property
|
|
27
|
+
def changes_past(self) -> bool:
|
|
28
|
+
"""Return `True` for all adjustments which are not `UNADJUSTED`.
|
|
29
|
+
|
|
30
|
+
A series holding these kind of adjustments need to be fully replaced
|
|
31
|
+
when a new adjustment comes.
|
|
32
|
+
"""
|
|
33
|
+
return self.name != "UNADJUSTED"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Unadjusted(Adjustment):
|
|
37
|
+
UNADJUSTED = "UNADJUSTED"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EquitiesAdjustment(Adjustment):
|
|
41
|
+
SPLIT = "adj_split"
|
|
42
|
+
SPLIT_AND_DIVIDEND = "adj_splitdiv"
|
|
43
|
+
UNADJUSTED = "UNADJUSTED"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ContinuousFuturesAdjustment(Adjustment):
|
|
47
|
+
RATIO = "contin_adj_ratio"
|
|
48
|
+
ABSOLUTE = "contin_adj_absolute"
|
|
49
|
+
UNADJUSTED = "contin_UNadj"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Timeframe(StrEnum):
|
|
53
|
+
MIN_1 = "1min"
|
|
54
|
+
MIN_5 = "5min"
|
|
55
|
+
MIN_30 = "30min"
|
|
56
|
+
HOUR_1 = "1hour"
|
|
57
|
+
DAY_1 = "1day"
|
|
58
|
+
|
|
59
|
+
def is_higher_than(self, other_timeframe: Self) -> bool:
|
|
60
|
+
order = list(Timeframe)
|
|
61
|
+
return order.index(self) > order.index(other_timeframe)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TradingHours(StrEnum):
|
|
68
|
+
ALL = auto()
|
|
69
|
+
REGULAR = auto()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Period(StrEnum):
|
|
73
|
+
FULL = auto()
|
|
74
|
+
MONTH = auto()
|
|
75
|
+
WEEK = auto()
|
|
76
|
+
DAY = auto()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OtherData(StrEnum):
|
|
80
|
+
SPLITS = auto()
|
|
81
|
+
DIVIDENDS = auto()
|
|
82
|
+
COMPANY_PROFILES = auto()
|
|
83
|
+
CONTRACT_DATES = "contin_audit"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ContractFiles(StrEnum):
|
|
87
|
+
"""Which half of the individual-contract dataset a request names.
|
|
88
|
+
|
|
89
|
+
The vendor splits it in two: the archive freezes everything up to 2025,
|
|
90
|
+
and the update carries the contracts trading from 2026, refreshed daily.
|
|
91
|
+
They name different contracts, so neither contains the other.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
ARCHIVE = auto()
|
|
95
|
+
UPDATE = auto()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ----------------------------------------------------------------------
|
|
99
|
+
# delisted data
|
|
100
|
+
# ----------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DelistedArchive(StrEnum):
|
|
104
|
+
"""One slice of the pre-2026 delisted history, downloaded on its own."""
|
|
105
|
+
|
|
106
|
+
# the docs list accepted values 1-5 but say "the four historical archives"
|
|
107
|
+
# in the same breath. The values win: a fifth archive that doesn't exist
|
|
108
|
+
# fails on the request, while omitting one that does loses data unseen.
|
|
109
|
+
ARCHIVE_1 = "1"
|
|
110
|
+
ARCHIVE_2 = "2"
|
|
111
|
+
ARCHIVE_3 = "3"
|
|
112
|
+
ARCHIVE_4 = "4"
|
|
113
|
+
ARCHIVE_5 = "5"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class DelistedUpdate(StrEnum):
|
|
117
|
+
"""The 2026+ delisted data, refreshed each Sunday at 11 PM, Eastern."""
|
|
118
|
+
|
|
119
|
+
WEEK = auto()
|
|
120
|
+
YEAR = auto()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
_DELISTED_SUFFIX = "-DELISTED"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class TickerListing:
|
|
11
|
+
# example structure of a ticker listing file: https://firstratedata.com/api_ticker_files/etf_ticker_dates_listing.txt
|
|
12
|
+
# Ticker,Name,First Date,Last Date
|
|
13
|
+
# AAA,Listed Funds Trust Aaf First Priority Clo Bond ETF,2020-09-09,2026-08-26
|
|
14
|
+
ticker: str
|
|
15
|
+
full_name: str
|
|
16
|
+
start_date: date
|
|
17
|
+
end_date: date
|
|
18
|
+
is_delisted: bool
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_csv(cls, csv_body: str) -> list[Self]:
|
|
22
|
+
# get one TickerListing for each row of file_body; the vendor heads the
|
|
23
|
+
# file with "Ticker,Name,First Date,Last Date"
|
|
24
|
+
rows = csv.reader(csv_body.splitlines())
|
|
25
|
+
listed_tickers = [
|
|
26
|
+
cls._from_row(row)
|
|
27
|
+
for row in rows
|
|
28
|
+
if any(row) and row[0].strip().casefold() != "ticker"
|
|
29
|
+
]
|
|
30
|
+
if not listed_tickers:
|
|
31
|
+
msg = f"ticker_listing answered with no rows: {csv_body}"
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
return listed_tickers
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# helpers
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_ROW_FIELDS = 4
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def _from_row(cls, row: list[str]) -> Self:
|
|
43
|
+
if len(row) < cls._ROW_FIELDS:
|
|
44
|
+
msg = (
|
|
45
|
+
f"ticker_listing row {','.join(row)!r} is not "
|
|
46
|
+
f"{{ticker}},{{name}},{{startDate}},{{endDate}}"
|
|
47
|
+
)
|
|
48
|
+
raise ValueError(
|
|
49
|
+
msg,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# from both ends rather than by position: an unquoted comma in a name
|
|
53
|
+
# ("Dow Jones Industrial Average, Total Return") splits into extra fields.
|
|
54
|
+
# Those fields belong to the name.
|
|
55
|
+
symbol, *full_name, start_date, end_date = row
|
|
56
|
+
is_delisted = symbol.endswith(_DELISTED_SUFFIX)
|
|
57
|
+
return cls(
|
|
58
|
+
symbol.removesuffix(_DELISTED_SUFFIX),
|
|
59
|
+
# strip the name whole rather than field by field: the space after
|
|
60
|
+
# the comma in "S&P 500, Total Return" belongs to the name
|
|
61
|
+
",".join(full_name).strip(),
|
|
62
|
+
cls._listing_date_from_str(start_date.strip(), row),
|
|
63
|
+
cls._listing_date_from_str(end_date.strip(), row),
|
|
64
|
+
is_delisted,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _listing_date_from_str(field: str, row: list[str]) -> date:
|
|
69
|
+
try:
|
|
70
|
+
return date.fromisoformat(field)
|
|
71
|
+
except ValueError as unreadable:
|
|
72
|
+
msg = (
|
|
73
|
+
f"ticker_listing row {','.join(row)!r} carries {field!r} "
|
|
74
|
+
"where a date belongs"
|
|
75
|
+
)
|
|
76
|
+
raise ValueError(
|
|
77
|
+
msg,
|
|
78
|
+
) from unreadable
|
|
File without changes
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from string import ascii_uppercase
|
|
4
|
+
|
|
5
|
+
from firstrate_data.domain import AssetType, BarType
|
|
6
|
+
from firstrate_data.domain.enums import (
|
|
7
|
+
ContinuousFuturesAdjustment,
|
|
8
|
+
ContractFiles,
|
|
9
|
+
DelistedArchive,
|
|
10
|
+
EquitiesAdjustment,
|
|
11
|
+
OtherData,
|
|
12
|
+
Period,
|
|
13
|
+
Timeframe,
|
|
14
|
+
Unadjusted,
|
|
15
|
+
)
|
|
16
|
+
from firstrate_data.download.requests import (
|
|
17
|
+
BarsRequest,
|
|
18
|
+
ContractBarsRequest,
|
|
19
|
+
DelistedBarsRequest,
|
|
20
|
+
NotOfferedError,
|
|
21
|
+
OtherDataRequest,
|
|
22
|
+
Request,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class BundleConfig:
|
|
28
|
+
"""Enough for Indices, FX and Crypto.
|
|
29
|
+
|
|
30
|
+
[Index docs](https://firstratedata.com/_readme/index.txt)
|
|
31
|
+
[FX docs](https://firstratedata.com/_readme/fx.txt)
|
|
32
|
+
[Crypto docs](https://firstratedata.com/_readme/crypto.txt)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
asset_type: AssetType
|
|
36
|
+
# None -> all the available timeframes
|
|
37
|
+
timeframes: Iterable[Timeframe] | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class EquitiesBundleConfig(BundleConfig):
|
|
42
|
+
"""Use this for ETFs.
|
|
43
|
+
|
|
44
|
+
[docs](https://firstratedata.com/_readme/etf.txt).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
adjustment: EquitiesAdjustment
|
|
48
|
+
# None behaves like ["A", ..., "Z"]
|
|
49
|
+
ticker_range: Iterable[str] | None
|
|
50
|
+
include_splits: bool
|
|
51
|
+
include_dividends: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
UNADJUSTED_DELISTED_TIMEFRAMES = (Timeframe.DAY_1, Timeframe.MIN_1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class StocksBundleConfig(EquitiesBundleConfig):
|
|
59
|
+
"""[Stocks docs](https://firstratedata.com/_readme/stock.txt)."""
|
|
60
|
+
|
|
61
|
+
include_company_profiles: bool
|
|
62
|
+
# false -> no archives are downloaded, true -> all of them,
|
|
63
|
+
# otherwise pass an iterable to select them.
|
|
64
|
+
include_delisted_archives: bool | Iterable[DelistedArchive]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class FuturesBundleConfig(BundleConfig):
|
|
69
|
+
"""[Future docs](https://firstratedata.com/_readme/futures.txt)."""
|
|
70
|
+
|
|
71
|
+
adjustment: ContinuousFuturesAdjustment
|
|
72
|
+
include_individual_contracts: bool | Iterable[Timeframe]
|
|
73
|
+
# Downloads a folder with one .txt per contract.
|
|
74
|
+
# Each row follows the YYYY-MM-DD,CONTRACT_CODE format.
|
|
75
|
+
# Useful to know rollover dates used by FirstRate in contract construction.
|
|
76
|
+
include_contract_dates: bool
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def bundle_requests(config: BundleConfig) -> Iterator[Request]:
|
|
80
|
+
"""Yield every request the bundle downloads, in download order."""
|
|
81
|
+
for timeframe in config.timeframes or tuple(Timeframe):
|
|
82
|
+
yield from _bar_requests(config, timeframe)
|
|
83
|
+
yield from _meta_requests(config)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _bar_requests(config: BundleConfig, timeframe: Timeframe) -> Iterator[Request]:
|
|
87
|
+
if isinstance(config, FuturesBundleConfig):
|
|
88
|
+
yield from _futures_requests(config, timeframe)
|
|
89
|
+
elif isinstance(config, EquitiesBundleConfig):
|
|
90
|
+
yield from _equities_requests(config, timeframe)
|
|
91
|
+
else:
|
|
92
|
+
# UNADJUSTED is the store's word for it, not the vendor's: index, FX
|
|
93
|
+
# and crypto carry no adjustment on the wire, and the bar type path
|
|
94
|
+
# names one at every level
|
|
95
|
+
yield from _offered(
|
|
96
|
+
lambda: BarsRequest(
|
|
97
|
+
BarType(
|
|
98
|
+
config.asset_type,
|
|
99
|
+
timeframe=timeframe,
|
|
100
|
+
adjustment=Unadjusted.UNADJUSTED,
|
|
101
|
+
),
|
|
102
|
+
Period.FULL,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _equities_requests(
|
|
108
|
+
config: EquitiesBundleConfig,
|
|
109
|
+
timeframe: Timeframe,
|
|
110
|
+
) -> Iterator[Request]:
|
|
111
|
+
bar_type = BarType(
|
|
112
|
+
config.asset_type,
|
|
113
|
+
timeframe=timeframe,
|
|
114
|
+
adjustment=config.adjustment,
|
|
115
|
+
)
|
|
116
|
+
for letter in config.ticker_range or ascii_uppercase:
|
|
117
|
+
yield from _offered(
|
|
118
|
+
lambda letter=letter: BarsRequest(
|
|
119
|
+
bar_type,
|
|
120
|
+
Period.FULL,
|
|
121
|
+
ticker_range=letter,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not isinstance(config, StocksBundleConfig):
|
|
126
|
+
return
|
|
127
|
+
for archive in _delisted_archives(selection=config.include_delisted_archives):
|
|
128
|
+
yield from _offered(
|
|
129
|
+
lambda archive=archive: DelistedBarsRequest(bar_type, selector=archive),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _futures_requests(
|
|
134
|
+
config: FuturesBundleConfig,
|
|
135
|
+
timeframe: Timeframe,
|
|
136
|
+
) -> Iterator[Request]:
|
|
137
|
+
yield from _offered(
|
|
138
|
+
lambda: BarsRequest(
|
|
139
|
+
BarType(
|
|
140
|
+
AssetType.FUTURES,
|
|
141
|
+
timeframe=timeframe,
|
|
142
|
+
adjustment=config.adjustment,
|
|
143
|
+
),
|
|
144
|
+
Period.FULL,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not _wants_contracts(timeframe, selection=config.include_individual_contracts):
|
|
149
|
+
return
|
|
150
|
+
# both halves name different contracts, so neither one contains the other
|
|
151
|
+
for half in ContractFiles:
|
|
152
|
+
yield ContractBarsRequest(
|
|
153
|
+
BarType(AssetType.FUTURES, timeframe=timeframe),
|
|
154
|
+
contract_files=half,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _meta_requests(config: BundleConfig) -> Iterator[Request]:
|
|
159
|
+
# ponytail: include_company_profiles is unserved -- the vendor documents no
|
|
160
|
+
# endpoint for it. Yield an OtherDataRequest here once one exists.
|
|
161
|
+
if isinstance(config, EquitiesBundleConfig):
|
|
162
|
+
if config.include_splits:
|
|
163
|
+
yield OtherDataRequest(config.asset_type, OtherData.SPLITS)
|
|
164
|
+
if config.include_dividends:
|
|
165
|
+
yield OtherDataRequest(config.asset_type, OtherData.DIVIDENDS)
|
|
166
|
+
if isinstance(config, FuturesBundleConfig) and config.include_contract_dates:
|
|
167
|
+
yield OtherDataRequest(AssetType.FUTURES, OtherData.CONTRACT_DATES)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _delisted_archives(
|
|
171
|
+
*,
|
|
172
|
+
selection: bool | Iterable[DelistedArchive],
|
|
173
|
+
) -> tuple[DelistedArchive, ...]:
|
|
174
|
+
if selection is True:
|
|
175
|
+
return tuple(DelistedArchive)
|
|
176
|
+
if selection is False:
|
|
177
|
+
return ()
|
|
178
|
+
return tuple(selection)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _wants_contracts(
|
|
182
|
+
timeframe: Timeframe,
|
|
183
|
+
*,
|
|
184
|
+
selection: bool | Iterable[Timeframe],
|
|
185
|
+
) -> bool:
|
|
186
|
+
if isinstance(selection, bool):
|
|
187
|
+
return selection
|
|
188
|
+
return timeframe in tuple(selection)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _offered(build: Callable[[], Request]) -> Iterator[Request]:
|
|
192
|
+
"""Yield the request, or nothing when the vendor doesn't serve it.
|
|
193
|
+
|
|
194
|
+
A bundle names timeframes and adjustments across a whole universe, and the
|
|
195
|
+
vendor serves some pairs and not others. The pairs it refuses are dropped
|
|
196
|
+
so the rest of the bundle still comes down.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
yield build()
|
|
200
|
+
except NotOfferedError:
|
|
201
|
+
return
|