"""FinzData custom-data classes for QuantConnect / LEAN. Copy into your project. In Initialize: FinzData.set_api_key(self.get_parameter("finzdata-api-key")) Then add data: self.add_data(FinzDataBars, "AAPL", Resolution.DAILY) See https://finzdata.com/quantconnect. Free keys return last 12 months. """ from AlgorithmImports import * import csv import urllib.parse class FinzData: BASE = "https://api.finzdata.com" _apikey = None @classmethod def set_api_key(cls, key): cls._apikey = key @classmethod def _url(cls, path, **params): params = {k: v for k, v in params.items() if v is not None} if not cls._apikey: raise ValueError("Set your FinzData API key: FinzData.set_api_key(...)") params = dict(params, format="csv", source="quantconnect", apikey=cls._apikey) return f"{cls.BASE}{path}?{urllib.parse.urlencode(params)}" def _live_start(date, is_live, days): """Live trading re-reads the source: fetch only recent rows. Backtests fetch full history once.""" return (date - timedelta(days=days)).strftime("%Y-%m-%d") if is_live else None class _FinzCsv(PythonData): _headers = {} # column map per symbol: one CSV header per subscription file def __init__(self): super().__init__() def get_source(self, config, date, is_live): raise NotImplementedError def reader(self, config, line, date, is_live): line = line.lstrip("\ufeff") if line else line # a BOM on the header row would hide "date" if not line or line.lstrip().startswith("{"): return None parts = next(csv.reader([line])) # quoted fields: COT market names contain commas key = (type(self).__name__, str(config.symbol.value)) first = parts[0].strip() if not (len(first) == 10 and first[4] == "-" and first[7] == "-" and first[:4].isdigit()) and not ( len(parts) > 1 and len(parts[1].strip()) == 10 and parts[1].strip()[4] == "-" and parts[1].strip()[:4].isdigit()): names = [n.strip() for n in parts] if "date" in names or "asof" in names: # only a real header row (never an error line) sets the column map _FinzCsv._headers[key] = {name: i for i, name in enumerate(names)} return None header = _FinzCsv._headers.get(key) if header is None or len(parts) != len(header): return None obj = type(self)() obj.symbol = config.symbol for name, idx in header.items(): raw = parts[idx].strip() if raw == "": val = None else: try: val = float(raw) except ValueError: val = raw obj[name] = val try: return self._adjust(obj, config) except (ValueError, TypeError, KeyError): return None # one malformed row never stops the subscription class FinzDataBars(_FinzCsv): def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/prices/daily", symbol=config.symbol.value, start=_live_start(date, is_live, 10)), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): dt = datetime.strptime(str(obj["date"])[:10], "%Y-%m-%d") obj.time = dt obj.end_time = dt.replace(hour=16, minute=0) obj.value = obj["close"] return obj class FinzDataGex(_FinzCsv): def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/gex/daily", ticker=config.symbol.value, start=_live_start(date, is_live, 10)), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): dt = datetime.strptime(obj["date"], "%Y-%m-%d") obj.time = dt obj.end_time = dt.replace(hour=17, minute=0) obj.value = obj["net_gex_bn"] return obj class FinzDataMacro(_FinzCsv): def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/macro", series=config.symbol.value, vintage="all"), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): try: obj["value"] = float(obj["value"]) except Exception: return None dt_obs = datetime.strptime(obj["date"], "%Y-%m-%d") dt_rt = datetime.strptime(obj["realtime_start"], "%Y-%m-%d") obj.time = dt_rt obj.end_time = dt_rt.replace(hour=16) # published at 8:30, 10:00 or 14:00 ET: usable from the close obj["observation_date"] = dt_obs obj.value = obj["value"] return obj class FinzDataCot(_FinzCsv): """Legacy futures-only COT report by CFTC contract code (e.g. "13874A" for E-mini S&P 500). value = non-commercial (speculator) net position. Tuesday positions become usable Friday 15:30 New York.""" def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/cot", report="legacy_fo", code=config.symbol.value, start=_live_start(date, is_live, 30)), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): try: net = float(obj["noncomm_positions_long_all"]) - float(obj["noncomm_positions_short_all"]) except Exception: return None dt = datetime.strptime(str(obj["date"])[:10], "%Y-%m-%d") obj.time = dt obj.end_time = dt + timedelta(days=3, hours=15, minutes=30) obj["net_noncommercial"] = net obj.value = net return obj class FinzDataFutures(_FinzCsv): def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/futures/daily", root=config.symbol.value, continuous="true", adjust="back", start=_live_start(date, is_live, 10)), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): dt = datetime.strptime(obj["date"], "%Y-%m-%d") obj.time = dt obj.end_time = dt.replace(hour=17, minute=0) obj.value = obj["close"] return obj class FinzDataRegime(_FinzCsv): """FinzData Quantitative Model regime labels, point in time. Symbol = series, e.g. "macro_quadrant", "risk_direction", "liquidity_direction", "credit_direction", "growth_direction", "inflation_direction". data[symbol]["label"] is the label as published; value codes it: +1 EASING/EXPANDING/ACCELERATING/ ABOVE_TREND/RISING, -1 TIGHTENING/CONTRACTING/DECELERATING/BELOW_TREND/FALLING, 0 FLAT; macro_quadrant 1 GOLDILOCKS, 2 REFLATION, 3 STAGFLATION, 4 SLOWDOWN. Usable at 16:00 ET on the asof date.""" CODES = {"EASING": 1, "EXPANDING": 1, "ACCELERATING": 1, "ABOVE_TREND": 1, "RISING": 1, "TIGHTENING": -1, "CONTRACTING": -1, "DECELERATING": -1, "BELOW_TREND": -1, "FALLING": -1, "FLAT": 0, "GOLDILOCKS": 1, "REFLATION": 2, "STAGFLATION": 3, "SLOWDOWN": 4} def get_source(self, config, date, is_live): return SubscriptionDataSource( FinzData._url("/v1/model/labels", series=config.symbol.value, start=_live_start(date, is_live, 30)), SubscriptionTransportMedium.REMOTE_FILE, FileFormat.CSV, ) def _adjust(self, obj, config): label = str(obj["label"]) if label not in self.CODES: return None dt = datetime.strptime(str(obj["asof"])[:10], "%Y-%m-%d") obj.time = dt obj.end_time = dt.replace(hour=16) obj["realized_label"] = None # known only in hindsight: never shown to an algorithm obj.value = self.CODES[label] return obj