├── tradeking ├── __init__.py ├── orders.py ├── utils.py ├── option.py └── api.py ├── setup.py ├── README.rst └── LICENSE /tradeking/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from tradeking.api import TradeKing 4 | from tradeking import option 5 | 6 | __all__ = ['TradeKing', 'option'] 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup 4 | 5 | version = '0.2' 6 | 7 | setup(name='tradeking', 8 | version=version, 9 | description="Python wrapper around Tradeking's API", 10 | long_description=open('README.rst').read(), 11 | keywords='', 12 | author='Jason Kölker', 13 | author_email='jason@koelker.net', 14 | url='https://github.com/jkoelker/python-tradeking', 15 | license='MIT', 16 | packages=['tradeking'], 17 | include_package_data=True, 18 | zip_safe=False, 19 | install_requires=[ 20 | 'lxml', 21 | 'requests', 22 | 'requests_oauthlib', 23 | 'pandas', 24 | ], 25 | ) 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A python wrapper for the Tradeking API 2 | ====================================== 3 | 4 | Usage 5 | ===== 6 | 7 | Obtain OAuth keys/secrets from 8 | `TradeKing `_. 9 | 10 | .. code-block:: python 11 | 12 | import tradeking 13 | 14 | CONSUMER_KEY = 'consumer_key' 15 | CONSUMER_SECRET = 'consumer_secret' 16 | OAUTH_TOKEN = 'oauth_token' 17 | OAUTH_SECRET = 'oauth_secret' 18 | 19 | tkapi = tradeking.TradeKing(consumer_key=CONSUMER_KEY, 20 | consumer_secret=CONSUMER_SECRET, 21 | oauth_token=OAUTH_TOKEN, 22 | oauth_secret=OAUTH_SECRET) 23 | 24 | quotes = tkapi.market.quotes('IBM') 25 | 26 | 27 | Note 28 | ==== 29 | 30 | In the near future the format of parsed results will return 31 | `Pandas `_ objects instead of dictionaries. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jason Kölker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tradeking/orders.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import functools 4 | 5 | from lxml import etree 6 | 7 | 8 | BUY_TO_COVER = '5' 9 | 10 | OPTION_CALL = 'OC' 11 | OPTION_PUT = 'OP' 12 | 13 | OPEN = 'O' 14 | CLOSE = 'C' 15 | 16 | STOCK = 'CS' 17 | OPTION = 'OPT' 18 | 19 | BUY = '1' 20 | SELL = '2' 21 | SELL_SHORT = '5' 22 | 23 | DAY = '0' 24 | GTC = '1' 25 | MOC = '2' 26 | 27 | MARKET = '1' 28 | LIMIT = '2' 29 | STOP = '3' 30 | STOP_LIMIT = '4' 31 | TRAILING_STOP = 'P' 32 | 33 | PRICE = '0' 34 | BASIS = '1' 35 | 36 | 37 | def Order(account, security_type, security, quantity, time_in_force=GTC, 38 | order_type=MARKET, side=BUY, trailing_stop_offset=None, 39 | trailing_stop_offset_type=PRICE, trailing_stop_peg_type='1'): 40 | fixml = etree.Element("FIXML", 41 | xmlns="http://www.fixprotocol.org/FIXML-5-0-SP2") 42 | order = etree.Element("Order", 43 | TmInForce=str(time_in_force), 44 | Typ=str(order_type), 45 | Side=str(side), 46 | Acct=str(account)) 47 | 48 | instrument = etree.Element("Instrmt", 49 | SecTyp=str(security_type), 50 | Sym=str(security)) 51 | 52 | order_quantity = etree.Element("OrdQty", 53 | Qty=str(quantity)) 54 | 55 | if trailing_stop_offset is not None: 56 | order.set('ExecInst' 'a') 57 | peg_instruction = etree.Element('PegInstr', 58 | OfstTyp=str(trailing_stop_offset_type), 59 | PegPxType=str(trailing_stop_peg_type), 60 | OfstVal=str(trailing_stop_offset)) 61 | order.append(peg_instruction) 62 | 63 | order.append(instrument) 64 | order.append(order_quantity) 65 | 66 | fixml.append(order) 67 | 68 | return fixml 69 | 70 | 71 | Buy = functools.partial(Order, side=BUY) 72 | Sell = functools.partial(Order, side=SELL) 73 | Short = functools.partial(Order, side=SELL_SHORT) 74 | 75 | # 76 | # 77 | # 78 | # 79 | # 80 | # 81 | -------------------------------------------------------------------------------- /tradeking/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import itertools 4 | import time 5 | 6 | import pandas as pd 7 | 8 | 9 | CALL = 'C' 10 | PUT = 'P' 11 | LONG = 'L' 12 | SHORT = 'S' 13 | 14 | 15 | class Price(int): 16 | BASE = 1000.0 17 | 18 | def __new__(cls, value=0): 19 | return int.__new__(cls, cls.encode(value)) 20 | 21 | def __str__(self): 22 | return self.__repr__() 23 | 24 | def __repr__(self): 25 | return self._decode().__repr__() 26 | 27 | @classmethod 28 | def encode(cls, value): 29 | return int(value * cls.BASE) 30 | 31 | @classmethod 32 | def decode(cls, value): 33 | return float(value) / cls.BASE 34 | 35 | def _decode(self): 36 | return self.decode(self.real) 37 | 38 | 39 | def option_symbol(underlying, expiration, call_put, strike): 40 | '''Format an option symbol from its component parts.''' 41 | call_put = call_put.upper() 42 | if call_put not in (CALL, PUT): 43 | raise ValueError("call_put value not one of ('%s', '%s'): %s" % 44 | (CALL, PUT, call_put)) 45 | 46 | expiration = pd.to_datetime(expiration).strftime('%y%m%d') 47 | 48 | strike = str(Price.encode(strike)).rstrip('L') 49 | strike = ('0' * (8 - len(strike))) + strike 50 | 51 | return '%s%s%s%s' % (underlying, expiration, call_put, strike) 52 | 53 | 54 | def option_symbols(underlying, expirations, strikes, calls=True, puts=True): 55 | '''Generate a list of option symbols for expirations and strikes.''' 56 | if not calls and not puts: 57 | raise ValueError('Either calls or puts must be true') 58 | 59 | call_put = '' 60 | 61 | if calls: 62 | call_put = call_put + CALL 63 | 64 | if puts: 65 | call_put = call_put + PUT 66 | 67 | return [option_symbol(*args) for args in 68 | itertools.product([underlying], expirations, call_put, strikes)] 69 | 70 | 71 | def parse_option_symbol(symbol): 72 | ''' 73 | Parse an option symbol into its component parts. 74 | 75 | returns (Underlying, Expiration, C/P, strike) 76 | ''' 77 | strike = Price.decode(symbol[-8:]) 78 | call_put = symbol[-9:-8].upper() 79 | expiration = pd.to_datetime(symbol[-15:-9]) 80 | underlying = symbol[:-15].upper() 81 | return underlying, expiration, call_put, strike 82 | 83 | 84 | # 85 | # © 2011 Christopher Arndt, MIT License 86 | # 87 | class cached_property(object): 88 | ''' 89 | Decorator for read-only properties evaluated only once within TTL period. 90 | 91 | It can be used to created a cached property like this:: 92 | 93 | import random 94 | 95 | # the class containing the property must be a new-style class 96 | class MyClass(object): 97 | # create property whose value is cached for ten minutes 98 | @cached_property(ttl=600) 99 | def randint(self): 100 | # will only be evaluated every 10 min. at maximum. 101 | return random.randint(0, 100) 102 | 103 | The value is cached in the '_cache' attribute of the object instance that 104 | has the property getter method wrapped by this decorator. The '_cache' 105 | attribute value is a dictionary which has a key for every property of the 106 | object which is wrapped by this decorator. Each entry in the cache is 107 | created only when the property is accessed for the first time and is a 108 | two-element tuple with the last computed property value and the last time 109 | it was updated in seconds since the epoch. 110 | 111 | The default time-to-live (TTL) is 300 seconds (5 minutes). Set the TTL to 112 | zero for the cached value to never expire. 113 | 114 | To expire a cached property value manually just do:: 115 | 116 | del instance._cache[] 117 | 118 | ''' 119 | def __init__(self, ttl=300): 120 | self.ttl = ttl 121 | 122 | def __call__(self, fget, doc=None): 123 | self.fget = fget 124 | self.__doc__ = doc or fget.__doc__ 125 | self.__name__ = fget.__name__ 126 | self.__module__ = fget.__module__ 127 | return self 128 | 129 | def __get__(self, inst, owner): 130 | now = time.time() 131 | try: 132 | value, last_update = inst._cache[self.__name__] 133 | if self.ttl > 0 and now - last_update > self.ttl: 134 | raise AttributeError 135 | except (KeyError, AttributeError): 136 | value = self.fget(inst) 137 | try: 138 | cache = inst._cache 139 | except AttributeError: 140 | cache = inst._cache = {} 141 | cache[self.__name__] = (value, now) 142 | return value 143 | -------------------------------------------------------------------------------- /tradeking/option.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | import pandas as pd 6 | 7 | from tradeking import api 8 | from tradeking import utils 9 | 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | def bid_ask_avg(symbol, quotes): 15 | mean = quotes[['bid', 'ask']].T.mean() 16 | return utils.Price(mean[symbol]) 17 | 18 | 19 | def tradeking_cost(num_legs, *args, **kwargs): 20 | base_fee = utils.Price(4.95) 21 | per_leg = utils.Price(0.65) 22 | return base_fee + per_leg * num_legs 23 | 24 | 25 | def tradeking_premium(tkapi=None, price_func=bid_ask_avg, **kwargs): 26 | if tkapi is None: 27 | consumer_key = kwargs.get('consumer_key') 28 | consumer_secret = kwargs.get('consumer_secret') 29 | oauth_token = kwargs.get('oauth_token') 30 | oauth_secret = kwargs.get('oauth_secret') 31 | 32 | if not all((consumer_key, consumer_secret, oauth_token, oauth_secret)): 33 | LOG.warning('No tkapi or tokens found. All premiums will be 0.') 34 | 35 | def zero(symbol, *args, **kwargs): 36 | return 0 37 | 38 | return zero 39 | 40 | tkapi = api.TradeKing(consumer_key=consumer_key, 41 | consumer_secret=consumer_secret, 42 | oauth_token=oauth_token, 43 | oauth_secret=oauth_secret) 44 | 45 | def premium(symbol, *args, **kwargs): 46 | quotes = tkapi.market.quotes(symbol) 47 | return price_func(symbol, quotes) 48 | 49 | return premium 50 | 51 | 52 | class Leg(object): 53 | def __init__(self, symbol, long_short=utils.LONG, expiration=None, 54 | call_put=None, strike=None, price_range=20, tick_size=0.01, 55 | cost_func=tradeking_cost, premium_func=None, **kwargs): 56 | 57 | if premium_func is None: 58 | premium_func = tradeking_premium(**kwargs) 59 | 60 | price_range = utils.Price(price_range) 61 | self._tick_size = utils.Price(tick_size) 62 | self._cost_func = cost_func 63 | self._premium_func = premium_func 64 | 65 | if not all((expiration, call_put, strike)): 66 | (symbol, expiration, 67 | call_put, strike) = utils.parse_option_symbol(symbol) 68 | 69 | self._symbol = utils.option_symbol(symbol, expiration, call_put, 70 | strike) 71 | self._underlying = symbol 72 | self._expiration = expiration 73 | self._call_put = call_put.upper() 74 | self._long_short = long_short.upper() 75 | self._strike = utils.Price(strike) 76 | self._start = self._strike - price_range 77 | self._stop = self._strike + price_range + 1 78 | 79 | if self._call_put == utils.PUT: 80 | self._payoff_func = lambda x: max(self._strike - x, 0) 81 | else: 82 | self._payoff_func = lambda x: max(x - self._strike, 0) 83 | 84 | def reset_start_stop(self, start, stop): 85 | if hasattr(self, '_cache') and 'payoffs' in self._cache: 86 | del self._cache['payoffs'] 87 | 88 | self._start = start 89 | self._stop = stop 90 | 91 | def payoff(self, price): 92 | ''' 93 | Evaluate the payoff for the leg at price. 94 | 95 | `price` *MUST* be a decimal shifted int. That is the price 7.95 is 96 | represented as 7950. Wrap the price in utils.Price to easily 97 | convert prior to passing to this function. 98 | 99 | E.g. 100 | 101 | price = 7.95 102 | price = utils.Price(price) 103 | 104 | 105 | returns a decimal shifted int/long. Use utils.Price.decode to convert 106 | to a float. 107 | 108 | E.g 109 | 110 | result = payoff(price) 111 | result = utils.Price.decode(result) 112 | ''' 113 | payoff = self._payoff_func(price) 114 | 115 | if self._long_short == utils.SHORT: 116 | payoff = payoff * -1 117 | 118 | return payoff 119 | 120 | @utils.cached_property() 121 | def payoffs(self): 122 | prices = pd.Series(range(self._start, self._stop, self._tick_size)) 123 | 124 | payoffs = prices.apply(self._payoff_func) 125 | payoffs.index = prices 126 | 127 | if self._long_short == utils.SHORT: 128 | payoffs = payoffs * -1 129 | return payoffs 130 | 131 | @utils.cached_property() 132 | def cost(self): 133 | return self._cost_func(1) 134 | 135 | @utils.cached_property() 136 | def premium(self): 137 | premium = self._premium_func(self._symbol) 138 | 139 | if self._long_short == utils.SHORT: 140 | premium = premium * -1 141 | 142 | return premium 143 | 144 | 145 | class MultiLeg(object): 146 | def __init__(self, *legs, **leg_kwargs): 147 | self._cost_func = leg_kwargs.pop('cost_func', tradeking_cost) 148 | self.__leg_kwargs = leg_kwargs 149 | self._legs = [] 150 | 151 | for leg in legs: 152 | self.add_leg(leg) 153 | 154 | def add_leg(self, leg, **leg_kwargs): 155 | ''' 156 | Add a leg to the MultiLeg. 157 | 158 | `leg` can either be an option symbol or an Leg instance. If it is 159 | an option symbol then either **leg_kwargs or the leg_kwargs to 160 | MultiLeg is used to construct the Leg, preferring **leg_kwargs. 161 | ''' 162 | if not isinstance(leg, Leg): 163 | if not leg_kwargs: 164 | leg_kwargs = self.__leg_kwargs 165 | 166 | leg = Leg(leg, **leg_kwargs) 167 | 168 | self._legs.append(leg) 169 | 170 | def payoff(self, price): 171 | ''' 172 | Evaluate the payoff for the MultiLeg at price. 173 | 174 | `price` *MUST* be a decimal shifted int/long. That is the price 7.95 is 175 | represented as 7950. Wrap the price in utils.Price to easily 176 | convert prior to passing to this function. 177 | 178 | E.g. 179 | 180 | price = 7.95 181 | price = utils.Price(price) 182 | 183 | 184 | returns a decimal shifted int/long. Use utils.Price.decode to convert 185 | to a float. 186 | 187 | E.g 188 | 189 | result = payoff(price) 190 | result = utils.Price.decode(result) 191 | ''' 192 | return sum([leg.payoff(price)for leg in self._legs]) 193 | 194 | @utils.cached_property() 195 | def payoffs(self): 196 | start = min([leg._start for leg in self._legs]) 197 | stop = max([leg._stop for leg in self._legs]) 198 | 199 | for leg in self._legs: 200 | leg.reset_start_stop(start, stop) 201 | 202 | payoffs = pd.Series() 203 | for leg in self._legs: 204 | payoffs = payoffs.add(leg.payoffs, fill_value=0) 205 | return payoffs 206 | 207 | @utils.cached_property() 208 | def cost(self): 209 | return self._cost_func(len(self._legs)) 210 | 211 | @utils.cached_property() 212 | def premium(self): 213 | return sum([leg.premium for leg in self._legs]) 214 | 215 | 216 | def _leg(symbol, long_short, call_put, expiration=None, strike=None, 217 | **leg_kwargs): 218 | 219 | if not all((expiration, strike)): 220 | (symbol, expiration, 221 | _call_put, strike) = utils.parse_option_symbol(symbol) 222 | 223 | return Leg(symbol, long_short=long_short, call_put=call_put, 224 | expiration=expiration, strike=strike, **leg_kwargs) 225 | 226 | 227 | def Call(symbol, long_short=utils.LONG, expiration=None, strike=None, 228 | **leg_kwargs): 229 | # NOTE(jkoelker) Ignore anything that was parsed, this is a Call 230 | call_put = utils.CALL 231 | return MultiLeg(_leg(symbol, long_short, call_put, expiration=expiration, 232 | strike=strike, **leg_kwargs), **leg_kwargs) 233 | 234 | 235 | def Put(symbol, long_short=utils.LONG, expiration=None, strike=None, 236 | **leg_kwargs): 237 | # NOTE(jkoelker) Ignore anything that was parsed, this is a Put 238 | call_put = utils.PUT 239 | return MultiLeg(_leg(symbol, long_short, call_put, expiration=expiration, 240 | strike=strike, **leg_kwargs), **leg_kwargs) 241 | 242 | 243 | def Straddle(symbol, long_short=utils.LONG, expiration=None, strike=None, 244 | **leg_kwargs): 245 | put = _leg(symbol, long_short=long_short, call_put=utils.PUT, 246 | expiration=expiration, strike=strike, **leg_kwargs) 247 | call = _leg(symbol, long_short=long_short, call_put=utils.CALL, 248 | expiration=expiration, strike=strike, **leg_kwargs) 249 | return MultiLeg(put, call, **leg_kwargs) 250 | 251 | 252 | def Strangle(symbol, call_strike, put_strike, long_short=utils.LONG, 253 | expiration=None, **leg_kwargs): 254 | if not expiration: 255 | (symbol, expiration, 256 | _call_put, _strike) = utils.parse_option_symbol(symbol) 257 | 258 | put = _leg(symbol, long_short=long_short, call_put=utils.PUT, 259 | expiration=expiration, strike=put_strike, **leg_kwargs) 260 | call = _leg(symbol, long_short=long_short, call_put=utils.CALL, 261 | expiration=expiration, strike=call_strike, **leg_kwargs) 262 | return MultiLeg(put, call, **leg_kwargs) 263 | 264 | 265 | def Collar(symbol, put_strike, call_strike, expiration=None, **leg_kwargs): 266 | 267 | if not expiration: 268 | (symbol, expiration, 269 | _call_put, _strike) = utils.parse_option_symbol(symbol) 270 | 271 | put = _leg(symbol, long_short=utils.LONG, call_put=utils.PUT, 272 | expiration=expiration, strike=put_strike, **leg_kwargs) 273 | call = _leg(symbol, long_short=utils.SHORT, call_put=utils.CALL, 274 | expiration=expiration, strike=call_strike, **leg_kwargs) 275 | 276 | return MultiLeg(put, call, **leg_kwargs) 277 | 278 | 279 | def plot(option, ypad=5, ylim=None, include_cost=True, include_premium=True, 280 | **kwargs): 281 | payoffs = option.payoffs 282 | index = [utils.Price.decode(i) for i in payoffs.index] 283 | 284 | if include_cost: 285 | payoffs = payoffs - option.cost 286 | 287 | if include_premium: 288 | payoffs = payoffs - option.premium 289 | 290 | payoffs = pd.Series([utils.Price.decode(i) for i in payoffs], 291 | index=index) 292 | 293 | if ylim is None: 294 | ylim = (payoffs.min() - ypad, payoffs.max() + ypad) 295 | 296 | return pd.tools.plotting.plot_series(payoffs, ylim=ylim, **kwargs) 297 | 298 | 299 | Leg.plot = plot 300 | MultiLeg.plot = plot 301 | -------------------------------------------------------------------------------- /tradeking/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import urllib.parse 4 | 5 | import requests_oauthlib as roauth 6 | import pandas as pd 7 | 8 | from tradeking import utils 9 | 10 | 11 | BASE_URL = 'https://api.tradeking.com/v1' 12 | _DATE_KEYS = ('date', 'datetime', 'divexdate', 'divpaydt', 'timestamp', 13 | 'pr_date', 'wk52hidate', 'wk52lodate', 'xdate') 14 | _FLOAT_KEYS = ('ask', 'bid', 'chg', 'cl', 'div', 'dollar_value', 'eps', 15 | 'hi', 'iad', 'idelta', 'igamma', 'imp_volatility', 'irho', 16 | 'itheta', 'ivega', 'last', 'lo', 'opn', 'opt_val', 'pchg', 17 | 'pcls', 'pe', 'phi', 'plo', 'popn', 'pr_adp_100', 'pr_adp_200', 18 | 'pr_adp_50', 'prbook', 'prchg', 'strikeprice', 'volatility12', 19 | 'vwap', 'wk52hi', 'wk52lo', 'yield') 20 | _INT_KEYS = ('asksz', 'basis', 'bidsz', 'bidtick', 'days_to_expiration', 21 | 'incr_vl', 'openinterest', 'pr_openinterest', 'prem_mult', 'pvol', 22 | 'sho', 'tr_num', 'vl', 'xday', 'xmonth', 'xyear') 23 | 24 | 25 | def _quotes_to_df(quotes): 26 | if not isinstance(quotes, list): 27 | quotes = [quotes] 28 | df = pd.DataFrame.from_records(quotes, index='symbol') 29 | 30 | for col in df.keys().intersection(_DATE_KEYS): 31 | kwargs = {} 32 | if col == 'timestamp': 33 | kwargs['unit'] = 's' 34 | 35 | try: 36 | df[col] = pd.to_datetime(df[col], **kwargs) 37 | except ValueError: 38 | pass 39 | 40 | for col in df.keys().intersection(_INT_KEYS): 41 | cleaned = df[col].str.replace(r'[$,%]', '') 42 | df[col] = cleaned.astype('int', errors='ignore') 43 | 44 | for col in df.keys().intersection(_FLOAT_KEYS): 45 | cleaned = df[col].str.replace(r'[$,%]', '') 46 | df[col] = cleaned.astype('float', errors='ignore') 47 | 48 | return df 49 | 50 | 51 | # TODO(jkoelker) Would be nice to do a proper DSL 52 | class OptionQuery(object): 53 | FIELDS = ('strikeprice', 'xdate', 'xmonth', 'xyear', 'put_call', 'unique') 54 | OPS = {'<': 'lt', 'lt': 'lt', 55 | '>': 'gt', 'gt': 'gt', 56 | '>=': 'gte', 'gte': 'gte', 57 | '<=': 'lte', 'lte': 'lte', 58 | '=': 'eq', '==': 'eq', 'eq': 'eq'} 59 | 60 | def __init__(self, query): 61 | if isinstance(query, str): 62 | query = [query] 63 | 64 | self._query = [] 65 | 66 | for part in query: 67 | field, op, value = part.split() 68 | field = field.lower() 69 | 70 | if field not in self.FIELDS or op not in self.OPS: 71 | continue 72 | 73 | if field == 'xdate': 74 | value = pd.to_datetime(value).strftime('%Y%m%d') 75 | 76 | self._query.append((field, self.OPS[op], value)) 77 | 78 | def __str__(self): 79 | return ' AND '.join(['%s-%s:%s' % (field, op, value) 80 | for field, op, value in self._query]) 81 | 82 | 83 | class API(object): 84 | def __init__(self, consumer_key, consumer_secret, 85 | oauth_token, oauth_secret): 86 | self._api = roauth.OAuth1Session(client_key=consumer_key, 87 | client_secret=consumer_secret, 88 | resource_owner_key=oauth_token, 89 | resource_owner_secret=oauth_secret) 90 | 91 | def join(self, *paths, **kwargs): 92 | if len(paths) == 1: 93 | paths = paths[0] 94 | 95 | if kwargs.get('clean', True): 96 | paths = [p.rstrip('/') for p in paths] 97 | 98 | return '/'.join(paths) 99 | 100 | def request(self, method, url, format='json', decode=True, **kwargs): 101 | if format: 102 | url = '.'.join((url, format)) 103 | 104 | r = self._api.request(method, url, **kwargs) 105 | 106 | if decode: 107 | r = r.json() 108 | 109 | return r 110 | 111 | def get(self, url, format='json', decode=True, **kwargs): 112 | return self.request('GET', url=url, format=format, decode=decode, 113 | **kwargs) 114 | 115 | def post(self, url, format='json', decode=True, **kwargs): 116 | return self.request('POST', url=url, format=format, decode=decode, 117 | **kwargs) 118 | 119 | 120 | class Account(object): 121 | def __init__(self, api, account_id): 122 | self._api = api 123 | self.account_id = account_id 124 | 125 | def _get(self, what=None, **kwargs): 126 | params = [BASE_URL, 'accounts', self.account_id] 127 | 128 | if what is not None: 129 | params.append(what) 130 | 131 | path = self._api.join(params) 132 | return self._api.get(path, **kwargs) 133 | 134 | def _balances(self, **kwargs): 135 | return self._get('balances', **kwargs) 136 | 137 | def _history(self, date_range='all', transactions='all', **kwargs): 138 | params = {'range': date_range, 'transactions': transactions} 139 | return self._get('history', params=params, **kwargs) 140 | 141 | def _holdings(self, **kwargs): 142 | return self._get('holdings', **kwargs) 143 | 144 | def _orders(self, **kwargs): 145 | return self._get('orders', **kwargs) 146 | 147 | @property 148 | def balances(self): 149 | r = self._balances() 150 | return r['response']['accountbalance'] 151 | 152 | def history(self, date_range='all', transactions='all'): 153 | r = self._history(date_range=date_range, transactions=transactions) 154 | return r['response']['transactions']['transaction'] 155 | 156 | @property 157 | def holdings(self): 158 | r = self._holdings() 159 | return r['response']['accountholdings']['holding'] 160 | 161 | # TODO(jkoelker) 162 | def order(self, order, preview=True): 163 | pass 164 | 165 | @property 166 | def orders(self): 167 | r = self._orders() 168 | return r['response']['orderstatus'] 169 | 170 | 171 | class News(object): 172 | def __init__(self, api): 173 | self._api = api 174 | 175 | def _article(self, article_id, **kwargs): 176 | path = self._api.join(BASE_URL, 'market', 'news', article_id) 177 | return self._api.get(path, **kwargs) 178 | 179 | def _search(self, keywords=None, symbols=None, maxhits=None, 180 | startdate=None, enddate=None, **kwargs): 181 | if not keywords and not symbols: 182 | raise ValueError('Either keywords or symbols are required') 183 | 184 | data = {} 185 | 186 | if keywords: 187 | if isinstance(keywords, str): 188 | keywords = [keywords] 189 | 190 | data['keywords'] = ','.join(keywords) 191 | 192 | if symbols: 193 | if isinstance(symbols, str): 194 | symbols = [symbols] 195 | 196 | data['symbols'] = ','.join(symbols) 197 | 198 | if maxhits: 199 | data['maxhits'] = maxhits 200 | 201 | # TODO(jkoelker) calculate enddate to be now() 202 | if (not startdate and enddate) or (not enddate and startdate): 203 | raise ValueError('Both startdate and endate are required if one ' 204 | 'is specified') 205 | 206 | if startdate and enddate: 207 | data['startdate'] = startdate 208 | data['enddate'] = enddate 209 | 210 | path = self._api.join(BASE_URL, 'market', 'news', 'search') 211 | return self._api.post(path, data=data, **kwargs) 212 | 213 | def article(self, article_id): 214 | r = self._article(article_id=article_id) 215 | return r['response']['article'] 216 | 217 | def search(self, keywords=None, symbols=None, maxhits=None, startdate=None, 218 | enddate=None): 219 | r = self._search(keywords=keywords, symbols=symbols, maxhits=maxhits, 220 | startdate=startdate, enddate=enddate) 221 | return r['response']['articles']['article'] 222 | 223 | 224 | class Options(object): 225 | def __init__(self, api, market): 226 | self._api = api 227 | self._market = market 228 | 229 | symbol = staticmethod(utils.option_symbol) 230 | symbols = staticmethod(utils.option_symbols) 231 | decode = staticmethod(utils.parse_option_symbol) 232 | 233 | def _expirations(self, symbol, **kwargs): 234 | params = {'symbol': symbol} 235 | path = self._api.join(BASE_URL, 'market', 'options', 'expirations') 236 | return self._api.get(path, params=params, **kwargs) 237 | 238 | def _search(self, symbol, query, fields=None, query_is_prepared=False, 239 | **kwargs): 240 | if not isinstance(query, OptionQuery) and not query_is_prepared: 241 | query = OptionQuery(query) 242 | 243 | data = {'symbol': symbol, 'query': query} 244 | 245 | if fields is not None: 246 | data['fids'] = ','.join(fields) 247 | 248 | path = self._api.join(BASE_URL, 'market', 'options', 'search') 249 | return self._api.post(path, data=data, **kwargs) 250 | 251 | def _strikes(self, symbol, **kwargs): 252 | params = {'symbol': symbol} 253 | path = self._api.join(BASE_URL, 'market', 'options', 'strikes') 254 | return self._api.get(path, params=params, **kwargs) 255 | 256 | def expirations(self, symbol): 257 | r = self._expirations(symbol=symbol) 258 | expirations = r['response']['expirationdates']['date'] 259 | return pd.to_datetime(pd.Series(expirations)) 260 | 261 | def search(self, symbol, query, fields=None): 262 | r = self._search(symbol=symbol, query=query, fields=fields) 263 | return _quotes_to_df(r['response']['quotes']['quote']) 264 | 265 | def strikes(self, symbol): 266 | r = self._strikes(symbol=symbol) 267 | strikes = r['response']['prices']['price'] 268 | return pd.Series(strikes, dtype=float) 269 | 270 | def quote(self, symbol, strikes=None, expirations=None, calls=True, 271 | puts=True, fields=None): 272 | if strikes is None: 273 | strikes = self.strikes(symbol) 274 | 275 | if expirations is None: 276 | expirations = self.expirations(symbol) 277 | 278 | symbols = utils.option_symbols(symbol, expirations, strikes, calls, 279 | puts) 280 | return self._market.quotes(symbols=symbols, fields=fields) 281 | 282 | 283 | class Market(object): 284 | def __init__(self, api): 285 | self._api = api 286 | self.news = News(self._api) 287 | self.options = Options(self._api, self) 288 | 289 | def _clock(self, **kwargs): 290 | path = self._api.join(BASE_URL, 'market', 'clock') 291 | return self._api.get(path, **kwargs) 292 | 293 | def _quotes(self, symbols, fields=None, **kwargs): 294 | if isinstance(symbols, (list, tuple)): 295 | symbols = ','.join(symbols) 296 | 297 | params = {'symbols': symbols} 298 | 299 | if fields is not None: 300 | params['fids'] = ','.join(fields) 301 | 302 | path = self._api.join(BASE_URL, 'market', 'ext', 'quotes') 303 | return self._api.post(path, data=params, **kwargs) 304 | 305 | def _toplist(self, list_type='toppctgainers', **kwargs): 306 | path = self._api.join(BASE_URL, 'market', 'toplists', list_type) 307 | return self._api.get(path, **kwargs) 308 | 309 | @property 310 | def clock(self): 311 | r = self._clock() 312 | r = r['response'] 313 | del r['@id'] 314 | return r 315 | 316 | def quotes(self, symbols, fields=None): 317 | r = self._quotes(symbols=symbols, fields=fields) 318 | return _quotes_to_df(r['response']['quotes']['quote']) 319 | 320 | def toplist(self, list_type='toppctgainers'): 321 | r = self._toplist(list_type=list_type) 322 | return _quotes_to_df(r['response']['quotes']['quote']) 323 | 324 | # TODO(jkoelker) market/timesales 325 | # TODO(jkoelker) market/quotes (iterator) 326 | 327 | 328 | class TradeKing(object): 329 | def __init__(self, consumer_key, consumer_secret, 330 | oauth_token, oauth_secret): 331 | self._api = API(consumer_key=consumer_key, 332 | consumer_secret=consumer_secret, 333 | oauth_token=oauth_token, 334 | oauth_secret=oauth_secret) 335 | self.market = Market(self._api) 336 | 337 | def _accounts(self, **kwargs): 338 | path = urllib.parse.urljoin(BASE_URL, 'accounts') 339 | return self._api.get(path, **kwargs) 340 | 341 | def account(self, account_id): 342 | return Account(self._api, account_id) 343 | 344 | # TODO(jkoelker) member/profile 345 | # TODO(jkoelker) utility/status 346 | # TODO(jkoelker) utility/version 347 | # TODO(jkoelker) utility/version 348 | # TODO(jkoelker) watchlists 349 | --------------------------------------------------------------------------------