diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..eb9a995 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +TDA_API_KEY= diff --git a/.gitignore b/.gitignore index 2093347..4737380 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ __pycache__/ -build -dist -*.egg-info -.eggs -.pypirc +build/ +dist/ +*.egg-info/ + +.env diff --git a/README.md b/README.md index cd68da9..a195be6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A library to fetch financial option chains and price options using closed-form s * 7/21/2017 Davis Edwards, refactored all of the functions to match the parameter order to Haug's "The Complete Guide to Option Pricing Formulas". * 08/31/2020 Daniel Rojas, Added functionality to fetch option chains using the TDAmeritrade API. * 09/05/2020 Daniel Rojas, Added functionality to fetch historical data using the TDAmeritrade API. +* 04/10/2021 Daniel Rojas, Write classes to represent API responses **TODO List** diff --git a/optlib/api.py b/optlib/api.py index cc67ff6..e0c09ff 100644 --- a/optlib/api.py +++ b/optlib/api.py @@ -1,17 +1,21 @@ from subprocess import check_output import json +from optlib.classes import Historical, OptionChain + # ------------------------------ # This class defines the URLs to the implemented endpoints. class Endpoint: CHAIN = "https://api.tdameritrade.com/v1/marketdata/chains" - HIST = "https://api.tdameritrade.com/v1/marketdata/{0}/pricehistory" + HISTORY = "https://api.tdameritrade.com/v1/marketdata/{0}/pricehistory" + INSTRUMENT = "https://api.tdameritrade.com/v1/instruments" + QUOTE = "https://api.tdameritrade.com/v1/marketdata/{0}/quotes" # ------------------------------ # This class defines the Exception that gets thrown when the api input is bad. class API_InputError(Exception): - def __init__(self, mismatch): - Exception.__init__(self, mismatch) + def __init__(self, msg): + Exception.__init__(self, msg) # ------------------------------ # This function verifies that the required apikey and symbol arguments are provided. @@ -25,7 +29,7 @@ def _get(endpoint, *args, **kwargs): url = "?".join([endpoint, "&".join(f"{k}={v}" for k, v in kwargs.items())]) if (resp := json.loads(check_output(["curl", "-gs", url]))).get("error"): - raise API_InputError(f"{0}".format(resp["error"])) + raise API_InputError("{0}".format(resp["error"])) return resp @@ -52,12 +56,12 @@ def get_chain(*args, **kwargs): optionType (str): Type of contracts to return. Default is ALL. Returns: - resp (dict): API response with option chain. + chain (OptionChain): API response with option chain. """ _test_input(*args, **kwargs) endpoint = Endpoint.CHAIN - return _get(endpoint, *args, **kwargs) + return OptionChain.parse(_get(endpoint, *args, **kwargs)) def get_historical(*args, **kwargs): """Request historical price data from TDAmeritrade's API. @@ -67,7 +71,7 @@ def get_historical(*args, **kwargs): symbol (str): Stock symbol to get the option chain for. periodType (str): The type of period to show. Can be day, month, year, or ytd. period (int): The number of periods to show. - frequencyType (str): The type of frequency with which a new candle is formed. Can be day, month, year, ytd. + frequencyType (str): The type of frequency with which a new candle is formed. Can be minute, daily, weekly, monthly. frequency (str): The number of the frequencyType to be included in each candle. startDate (int): Start date as milliseconds since epoch. endDate (int): End date as milliseconds since epoch. @@ -78,5 +82,35 @@ def get_historical(*args, **kwargs): """ _test_input(*args, **kwargs) - endpoint = Endpoint.HIST.format(kwargs["symbol"]) + endpoint = Endpoint.HISTORY.format(kwargs["symbol"]) + return Historical.parse(_get(endpoint, *args, **kwargs)) + +def get_fundamental(*args, **kwargs): + """Retrieve fundamental data. + + Args: + apikey (str): API key to api.tdameritrade.com. + symbol (str): Stock symbol to get the option chain for. + + Returns: + resp (dict): API response with fundamentals data. + """ + _test_input(*args, **kwargs) + + kwargs.update({"projection": "fundamental"}) + return _get(Endpoint.INSTRUMENT, *args, **kwargs) + +def get_quote(*args, **kwargs): + """Get quote for a symbol. + + Args: + apikey (str): API key to api.tdameritrade.com. + symbol (str): Stock symbol to get the option chain for. + + Returns: + resp (dict): API response with price quote. + """ + _test_input(*args, **kwargs) + + endpoint = Endpoint.QUOTE.format(kwargs["symbol"]) return _get(endpoint, *args, **kwargs) diff --git a/optlib/classes.py b/optlib/classes.py new file mode 100644 index 0000000..7f0a05f --- /dev/null +++ b/optlib/classes.py @@ -0,0 +1,107 @@ +from datetime import datetime +import pandas as pd +import json + +class Historical: + + def __init__(self, data): + self.symbol = data["symbol"] + self.empty = data["empty"] + + self.candles = [] + for c in data["candles"]: + c["datetime"] = datetime.utcfromtimestamp(c["datetime"] / 1000) + self.candles.append(c) + + @classmethod + def parse(cls, resp): + return Historical(resp) + + def to_dataframe(self): + return pd.DataFrame([c for c in self.candles]) + + def __iter__(self): + for c in self.candles: + yield c + +class Option: + + parseDateCols = ( + "tradeTimeInLong", + "quoteTimeInLong", + "expirationDate", + "lastTradingDay", + ) + + def __init__(self, data): + self.dict = {} + for k, v in data.items(): + if k in self.parseDateCols and v: + v = datetime.utcfromtimestamp(v / 1000) + self.dict.update({k: v}) + setattr(self, k, v) + + def to_dict(self): + return self.dict + +class OptionChain: + + def __init__( + self, + symbol, + isDelayed, + isIndex, + interestRate, + underlyingPrice, + volatility, + callExpDateMap, + putExpDateMap + ): + self.symbol = symbol + self.isDelayed = isDelayed + self.isIndex = isIndex + self.interestRate = interestRate + self.underlyingPrice = underlyingPrice + self.volatility = volatility + self.expDateMap = tuple(callExpDateMap.items()) + tuple(putExpDateMap.items()) + + @classmethod + def parse(cls, resp): + + if resp.get("status") != "SUCCESS": + raise ValueError("No successful response from chain API.") + + if (strategy := resp.get("strategy")) != "SINGLE": + raise ValueError(f"Strategy ({strategy}) is not supported. Use strategy 'SINGLE'.") + + return OptionChain( + symbol=resp["symbol"], + isDelayed=resp["isDelayed"], + isIndex=resp["isIndex"], + interestRate=resp["interestRate"], + underlyingPrice=resp["underlyingPrice"], + volatility=resp["volatility"], + callExpDateMap=resp.get("callExpDateMap", dict()), + putExpDateMap=resp.get("putExpDateMap", dict()) + ) + + @classmethod + def from_json(cls, filepath): + + with open(filepath, "r") as f: + resp = json.load(f) + + return cls.parse(resp) + + @property + def options(self): + return list(self) + + def to_dataframe(self): + return pd.DataFrame([opt.to_dict() for opt in self.options]) + + def __iter__(self): + for expiryDate, strikes in self.expDateMap: + for strikePrice, data in strikes.items(): + for r in data: + yield Option(r) diff --git a/setup.py b/setup.py index 8847540..b9a00ef 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="optlib", - version="0.3.0", + version="0.4.0", description="A library for financial options pricing written in Python.", url="http://github.com/bartolomed/optlib", author="Davis Edwards & Daniel Rojas",