Skip to content

Commit

Permalink
Merge pull request #39 from alpacahq/marketDataApi
Browse files Browse the repository at this point in the history
Add accessor for new bars data endpoint
  • Loading branch information
ttt733 authored Nov 28, 2018
2 parents 712de48 + 1ddac28 commit 1e376a0
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 258 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ is for live trading, and for paper trading and other purposes, you can to change
the base URL. You can pass it as an argument `REST()`, or using the environment
variable, `APCA_API_BASE_URL`.

The environment variable `APCA_API_DATA_URL` can also be changed to configure the
endpoint for returning data from the `/bars` endpoint. By default, it will use
`https://data.alpaca.markets`.

## REST

The `REST` class is the entry point for the API request. The instance of this
class provides all REST API calls such as account, orders, positions,
bars, quotes and fundamentals.
and bars.

Each returned object is wrapped by a subclass of `Entity` class (or a list of it).
This helper class provides property access (the "dot notation") to the
Expand Down Expand Up @@ -124,6 +128,15 @@ Calls `GET /assets` and returns a list of `Asset` entities.
### REST.get_asset(symbol)
Calls `GET /assets/{symbol}` and returns an `Asset` entity.

### REST.get_barset(symbols, timeframe, limit, start=None, end=None, after=None, until=None)
Calls `GET /bars/{timeframe}` for the given symbols, and returns a Barset with `limit` Bar objects
for each of the the requested symbols.
`timeframe` can be one of `minute`, `1Min`, `5Min`, `15Min`, `day` or `1D`. `minute` is an alias
of `1Min`. Similarly, `day` is an alias of `1D`.
`start`, `end`, `after`, and `until` need to be string format, which you can obtain with
`pd.Timestamp().isoformat()`
`after` cannot be used with `start` and `until` cannot be used with `end`.

### REST.get_clock()
Calls `GET /clock` and returns a `Clock` entity.

Expand Down Expand Up @@ -246,7 +259,7 @@ Returns a `Trades` which is a list of `Trade` entities.
Returns a pandas DataFrame object with the ticks returned by the `historic_trades`.

### polygon/REST.historic_quotes(symbol, date, offset=None, limit=None)
Returns a `Quotes` which is a list of `Quote` entities.
Returns a `Quotes` which is a list of `Quote` entities.

- `date` is a date string such as '2018-2-2'. The returned quotes are from this day only.
- `offset` is an integer in Unix Epoch millisecond as the lower bound filter, inclusive.
Expand Down
5 changes: 5 additions & 0 deletions alpaca_trade_api/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ def get_base_url():
'APCA_API_BASE_URL', 'https://api.alpaca.markets').rstrip('/')


def get_data_url():
return os.environ.get(
'APCA_API_DATA_URL', 'https://data.alpaca.markets').rstrip('/')


def get_credentials(key_id=None, secret_key=None):
key_id = key_id or os.environ.get('APCA_API_KEY_ID')
if key_id is None:
Expand Down
86 changes: 49 additions & 37 deletions alpaca_trade_api/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re

ISO8601YMD = re.compile(r'\d{4}-\d{2}-\d{2}T')
NY = 'America/New_York'


class Entity(object):
Expand All @@ -25,7 +26,7 @@ def __getattr__(self, key):
return pd.Timestamp(val)
else:
return val
return getattr(super(), key)
return super().__getattribute__(key)

def __repr__(self):
return '{name}({raw})'.format(
Expand All @@ -51,51 +52,62 @@ class Position(Entity):


class Bar(Entity):
pass
def __getattr__(self, key):
if key == 't':
val = self._raw[key[0]]
return pd.Timestamp(val, unit='s', tz=NY)
return super().__getattr__(key)


class AssetBars(Entity):
class Bars(list):
def __init__(self, raw):
super().__init__([Bar(o) for o in raw])
self._raw = raw

@property
def df(self):
if not hasattr(self, '_df'):
df = pd.DataFrame(self._raw['bars'])
if len(df.columns) == 0:
df.columns = ('time', 'open', 'high', 'low', 'close', 'volume')
df = df.set_index('time')
df.index = pd.to_datetime(df.index)
df = pd.DataFrame(
self._raw, columns=('t', 'o', 'h', 'l', 'c', 'v'),
)
alias = {
't': 'time',
'o': 'open',
'h': 'high',
'l': 'low',
'c': 'close',
'v': 'volume',
}
df.columns = [alias[c] for c in df.columns]
df.set_index('time', inplace=True)
df.index = pd.to_datetime(
df.index * 1e9, utc=True,
).tz_convert(NY)
self._df = df
return self._df

@property
def bars(self):
if not hasattr(self, '_bars'):
raw = self._raw
t = []
o = []
h = []
l = [] # noqa: E741
c = []
v = []
bars = []
for bar in raw['bars']:
t.append(pd.Timestamp(bar['time']))
o.append(bar['open'])
h.append(bar['high'])
l.append(bar['low'])
c.append(bar['close'])
v.append(bar['volume'])
bars.append(Bar(bar))
self._bars = bars
return self._bars


class Quote(Entity):
pass

class BarSet(dict):
def __init__(self, raw):
for symbol in raw:
self[symbol] = Bars(raw[symbol])
self._raw = raw

class Fundamental(Entity):
pass
@property
def df(self):
'''## Experimental '''
if not hasattr(self, '_df'):
dfs = []
for symbol, bars in self.items():
df = bars.df.copy()
df.columns = pd.MultiIndex.from_product(
[[symbol, ], df.columns])
dfs.append(df)
if len(dfs) == 0:
self._df = pd.DataFrame()
else:
self._df = pd.concat(dfs, axis=1)
return self._df


class Clock(Entity):
Expand All @@ -106,7 +118,7 @@ def __getattr__(self, key):
return pd.Timestamp(val)
else:
return val
return getattr(super(), key)
return super().__getattr__(key)


class Calendar(Entity):
Expand All @@ -119,4 +131,4 @@ def __getattr__(self, key):
return pd.Timestamp(val).time()
else:
return val
return getattr(super(), key)
return super().__getattr__(key)
108 changes: 37 additions & 71 deletions alpaca_trade_api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import requests
from requests.exceptions import HTTPError
import time
from .common import get_base_url, get_credentials
from .common import (
get_base_url,
get_data_url,
get_credentials,
)
from .entity import (
Account, Asset, Order, Position,
AssetBars, Quote, Fundamental,
Clock, Calendar,
BarSet, Clock, Calendar,
)
from . import polygon

Expand Down Expand Up @@ -61,8 +64,9 @@ def __init__(self, key_id=None, secret_key=None, base_url=None):
self.polygon = polygon.REST(
self._key_id, 'staging' in self._base_url)

def _request(self, method, path, data=None, prefix='/v1'):
url = self._base_url + prefix + path
def _request(self, method, path, data=None, prefix='/v1', base_url=None):
base_url = base_url or self._base_url
url = base_url + prefix + path
headers = {
'APCA-API-KEY-ID': self._key_id,
'APCA-API-SECRET-KEY': self._secret_key,
Expand All @@ -88,7 +92,7 @@ def _request(self, method, path, data=None, prefix='/v1'):
return self._one_request(method, url, opts, retry)
except RetryException:
retry_wait = self._retry_wait
logger.warn(
logger.warning(
'sleep {} seconds and retrying {} '
'{} more time(s)...'.format(
retry_wait, url, retry))
Expand Down Expand Up @@ -130,6 +134,10 @@ def post(self, path, data=None):
def delete(self, path, data=None):
return self._request('DELETE', path, data)

def data_get(self, path, data=None):
base_url = get_data_url()
return self._request('GET', path, data, base_url=base_url)

def get_account(self):
'''Get the account'''
resp = self.get('/account')
Expand Down Expand Up @@ -215,78 +223,36 @@ def get_asset(self, symbol):
resp = self.get('/assets/{}'.format(symbol))
return Asset(resp)

def list_quotes(self, symbols):
'''Get a list of quotes'''
if not isinstance(symbols, str):
symbols = ','.join(symbols)
params = {
'symbols': symbols,
}
resp = self.get('/quotes', params)
return [Quote(o) for o in resp]

def get_quote(self, symbol):
'''Get a quote'''
resp = self.get('/assets/{}/quote'.format(symbol))
return Quote(resp)

def list_fundamentals(self, symbols):
'''Get a list of fundamentals'''
if not isinstance(symbols, str):
symbols = ','.join(symbols)
params = {
'symbols': symbols,
}
resp = self.get('/fundamentals', params)
return [Fundamental(o) for o in resp]

def get_fundamental(self, symbol):
'''Get a fundamental'''
resp = self.get('/assets/{}/fundamental'.format(symbol))
return Fundamental(resp)

def list_bars(
self,
symbols,
timeframe,
start_dt=None,
end_dt=None,
limit=None):
'''Get a list of bars'''
def get_barset(self,
symbols,
timeframe,
limit=None,
start=None,
end=None,
after=None,
until=None):
'''Get BarSet(dict[str]->list[Bar])
The parameter symbols can be either a comma-split string
or a list of string. Each symbol becomes the key of
the returned value.
'''
if not isinstance(symbols, str):
symbols = ','.join(symbols)
params = {
'symbols': symbols,
'timeframe': timeframe,
}
if start_dt is not None:
params['start_dt'] = start_dt
if end_dt is not None:
params['end_dt'] = end_dt
if limit is not None:
params['limit'] = limit
resp = self.get('/bars', params)
return [AssetBars(o) for o in resp]

def get_bars(
self,
symbol,
timeframe,
start_dt=None,
end_dt=None,
limit=None):
'''Get bars'''
params = {
'timeframe': timeframe,
}
if start_dt is not None:
params['start_dt'] = start_dt
if end_dt is not None:
params['end_dt'] = end_dt
if limit is not None:
params['limit'] = limit
resp = self.get('/assets/{}/bars'.format(symbol), params)
return AssetBars(resp)
if start is not None:
params['start'] = start
if end is not None:
params['end'] = end
if after is not None:
params['after'] = after
if until is not None:
params['until'] = until
resp = self.data_get('/bars/{}'.format(timeframe), params)
return BarSet(resp)

def get_clock(self):
resp = self.get('/clock')
Expand Down
6 changes: 1 addition & 5 deletions alpaca_trade_api/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
import websocket
from .common import get_base_url, get_credentials
from .entity import Account, AssetBars, Quote, Entity
from .entity import Account, Entity


class StreamConn(object):
Expand Down Expand Up @@ -55,10 +55,6 @@ def run(self):
def _cast(self, stream, msg):
if stream == 'account_updates':
return Account(msg)
elif re.match(r'^bars/', stream):
return AssetBars(msg)
elif re.match(r'^quotes/', stream):
return Quote(msg)
return Entity(msg)

def _dispatch(self, stream, msg):
Expand Down
6 changes: 1 addition & 5 deletions alpaca_trade_api/stream2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
import websockets
from .common import get_base_url, get_credentials
from .entity import Account, AssetBars, Quote, Entity
from .entity import Account, Entity
from . import polygon


Expand Down Expand Up @@ -109,10 +109,6 @@ async def close(self):
def _cast(self, channel, msg):
if channel == 'account_updates':
return Account(msg)
elif re.match(r'^bars/', channel):
return AssetBars(msg)
elif re.match(r'^quotes/', channel):
return Quote(msg)
return Entity(msg)

async def _dispatch_nats(self, conn, subject, data):
Expand Down
Loading

0 comments on commit 1e376a0

Please sign in to comment.