Skip to content

Commit 1e376a0

Browse files
authored
Merge pull request #39 from alpacahq/marketDataApi
Add accessor for new bars data endpoint
2 parents 712de48 + 1ddac28 commit 1e376a0

File tree

8 files changed

+140
-258
lines changed

8 files changed

+140
-258
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ is for live trading, and for paper trading and other purposes, you can to change
5252
the base URL. You can pass it as an argument `REST()`, or using the environment
5353
variable, `APCA_API_BASE_URL`.
5454

55+
The environment variable `APCA_API_DATA_URL` can also be changed to configure the
56+
endpoint for returning data from the `/bars` endpoint. By default, it will use
57+
`https://data.alpaca.markets`.
58+
5559
## REST
5660

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

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

131+
### REST.get_barset(symbols, timeframe, limit, start=None, end=None, after=None, until=None)
132+
Calls `GET /bars/{timeframe}` for the given symbols, and returns a Barset with `limit` Bar objects
133+
for each of the the requested symbols.
134+
`timeframe` can be one of `minute`, `1Min`, `5Min`, `15Min`, `day` or `1D`. `minute` is an alias
135+
of `1Min`. Similarly, `day` is an alias of `1D`.
136+
`start`, `end`, `after`, and `until` need to be string format, which you can obtain with
137+
`pd.Timestamp().isoformat()`
138+
`after` cannot be used with `start` and `until` cannot be used with `end`.
139+
127140
### REST.get_clock()
128141
Calls `GET /clock` and returns a `Clock` entity.
129142

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

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

251264
- `date` is a date string such as '2018-2-2'. The returned quotes are from this day only.
252265
- `offset` is an integer in Unix Epoch millisecond as the lower bound filter, inclusive.

alpaca_trade_api/common.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ def get_base_url():
66
'APCA_API_BASE_URL', 'https://api.alpaca.markets').rstrip('/')
77

88

9+
def get_data_url():
10+
return os.environ.get(
11+
'APCA_API_DATA_URL', 'https://data.alpaca.markets').rstrip('/')
12+
13+
914
def get_credentials(key_id=None, secret_key=None):
1015
key_id = key_id or os.environ.get('APCA_API_KEY_ID')
1116
if key_id is None:

alpaca_trade_api/entity.py

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44

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

78

89
class Entity(object):
@@ -25,7 +26,7 @@ def __getattr__(self, key):
2526
return pd.Timestamp(val)
2627
else:
2728
return val
28-
return getattr(super(), key)
29+
return super().__getattribute__(key)
2930

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

5253

5354
class Bar(Entity):
54-
pass
55+
def __getattr__(self, key):
56+
if key == 't':
57+
val = self._raw[key[0]]
58+
return pd.Timestamp(val, unit='s', tz=NY)
59+
return super().__getattr__(key)
5560

5661

57-
class AssetBars(Entity):
62+
class Bars(list):
63+
def __init__(self, raw):
64+
super().__init__([Bar(o) for o in raw])
65+
self._raw = raw
5866

5967
@property
6068
def df(self):
6169
if not hasattr(self, '_df'):
62-
df = pd.DataFrame(self._raw['bars'])
63-
if len(df.columns) == 0:
64-
df.columns = ('time', 'open', 'high', 'low', 'close', 'volume')
65-
df = df.set_index('time')
66-
df.index = pd.to_datetime(df.index)
70+
df = pd.DataFrame(
71+
self._raw, columns=('t', 'o', 'h', 'l', 'c', 'v'),
72+
)
73+
alias = {
74+
't': 'time',
75+
'o': 'open',
76+
'h': 'high',
77+
'l': 'low',
78+
'c': 'close',
79+
'v': 'volume',
80+
}
81+
df.columns = [alias[c] for c in df.columns]
82+
df.set_index('time', inplace=True)
83+
df.index = pd.to_datetime(
84+
df.index * 1e9, utc=True,
85+
).tz_convert(NY)
6786
self._df = df
6887
return self._df
6988

70-
@property
71-
def bars(self):
72-
if not hasattr(self, '_bars'):
73-
raw = self._raw
74-
t = []
75-
o = []
76-
h = []
77-
l = [] # noqa: E741
78-
c = []
79-
v = []
80-
bars = []
81-
for bar in raw['bars']:
82-
t.append(pd.Timestamp(bar['time']))
83-
o.append(bar['open'])
84-
h.append(bar['high'])
85-
l.append(bar['low'])
86-
c.append(bar['close'])
87-
v.append(bar['volume'])
88-
bars.append(Bar(bar))
89-
self._bars = bars
90-
return self._bars
91-
92-
93-
class Quote(Entity):
94-
pass
9589

90+
class BarSet(dict):
91+
def __init__(self, raw):
92+
for symbol in raw:
93+
self[symbol] = Bars(raw[symbol])
94+
self._raw = raw
9695

97-
class Fundamental(Entity):
98-
pass
96+
@property
97+
def df(self):
98+
'''## Experimental '''
99+
if not hasattr(self, '_df'):
100+
dfs = []
101+
for symbol, bars in self.items():
102+
df = bars.df.copy()
103+
df.columns = pd.MultiIndex.from_product(
104+
[[symbol, ], df.columns])
105+
dfs.append(df)
106+
if len(dfs) == 0:
107+
self._df = pd.DataFrame()
108+
else:
109+
self._df = pd.concat(dfs, axis=1)
110+
return self._df
99111

100112

101113
class Clock(Entity):
@@ -106,7 +118,7 @@ def __getattr__(self, key):
106118
return pd.Timestamp(val)
107119
else:
108120
return val
109-
return getattr(super(), key)
121+
return super().__getattr__(key)
110122

111123

112124
class Calendar(Entity):
@@ -119,4 +131,4 @@ def __getattr__(self, key):
119131
return pd.Timestamp(val).time()
120132
else:
121133
return val
122-
return getattr(super(), key)
134+
return super().__getattr__(key)

alpaca_trade_api/rest.py

Lines changed: 37 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import requests
44
from requests.exceptions import HTTPError
55
import time
6-
from .common import get_base_url, get_credentials
6+
from .common import (
7+
get_base_url,
8+
get_data_url,
9+
get_credentials,
10+
)
711
from .entity import (
812
Account, Asset, Order, Position,
9-
AssetBars, Quote, Fundamental,
10-
Clock, Calendar,
13+
BarSet, Clock, Calendar,
1114
)
1215
from . import polygon
1316

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

64-
def _request(self, method, path, data=None, prefix='/v1'):
65-
url = self._base_url + prefix + path
67+
def _request(self, method, path, data=None, prefix='/v1', base_url=None):
68+
base_url = base_url or self._base_url
69+
url = base_url + prefix + path
6670
headers = {
6771
'APCA-API-KEY-ID': self._key_id,
6872
'APCA-API-SECRET-KEY': self._secret_key,
@@ -88,7 +92,7 @@ def _request(self, method, path, data=None, prefix='/v1'):
8892
return self._one_request(method, url, opts, retry)
8993
except RetryException:
9094
retry_wait = self._retry_wait
91-
logger.warn(
95+
logger.warning(
9296
'sleep {} seconds and retrying {} '
9397
'{} more time(s)...'.format(
9498
retry_wait, url, retry))
@@ -130,6 +134,10 @@ def post(self, path, data=None):
130134
def delete(self, path, data=None):
131135
return self._request('DELETE', path, data)
132136

137+
def data_get(self, path, data=None):
138+
base_url = get_data_url()
139+
return self._request('GET', path, data, base_url=base_url)
140+
133141
def get_account(self):
134142
'''Get the account'''
135143
resp = self.get('/account')
@@ -215,78 +223,36 @@ def get_asset(self, symbol):
215223
resp = self.get('/assets/{}'.format(symbol))
216224
return Asset(resp)
217225

218-
def list_quotes(self, symbols):
219-
'''Get a list of quotes'''
220-
if not isinstance(symbols, str):
221-
symbols = ','.join(symbols)
222-
params = {
223-
'symbols': symbols,
224-
}
225-
resp = self.get('/quotes', params)
226-
return [Quote(o) for o in resp]
227-
228-
def get_quote(self, symbol):
229-
'''Get a quote'''
230-
resp = self.get('/assets/{}/quote'.format(symbol))
231-
return Quote(resp)
232-
233-
def list_fundamentals(self, symbols):
234-
'''Get a list of fundamentals'''
235-
if not isinstance(symbols, str):
236-
symbols = ','.join(symbols)
237-
params = {
238-
'symbols': symbols,
239-
}
240-
resp = self.get('/fundamentals', params)
241-
return [Fundamental(o) for o in resp]
242-
243-
def get_fundamental(self, symbol):
244-
'''Get a fundamental'''
245-
resp = self.get('/assets/{}/fundamental'.format(symbol))
246-
return Fundamental(resp)
247-
248-
def list_bars(
249-
self,
250-
symbols,
251-
timeframe,
252-
start_dt=None,
253-
end_dt=None,
254-
limit=None):
255-
'''Get a list of bars'''
226+
def get_barset(self,
227+
symbols,
228+
timeframe,
229+
limit=None,
230+
start=None,
231+
end=None,
232+
after=None,
233+
until=None):
234+
'''Get BarSet(dict[str]->list[Bar])
235+
The parameter symbols can be either a comma-split string
236+
or a list of string. Each symbol becomes the key of
237+
the returned value.
238+
'''
256239
if not isinstance(symbols, str):
257240
symbols = ','.join(symbols)
258241
params = {
259242
'symbols': symbols,
260-
'timeframe': timeframe,
261243
}
262-
if start_dt is not None:
263-
params['start_dt'] = start_dt
264-
if end_dt is not None:
265-
params['end_dt'] = end_dt
266244
if limit is not None:
267245
params['limit'] = limit
268-
resp = self.get('/bars', params)
269-
return [AssetBars(o) for o in resp]
270-
271-
def get_bars(
272-
self,
273-
symbol,
274-
timeframe,
275-
start_dt=None,
276-
end_dt=None,
277-
limit=None):
278-
'''Get bars'''
279-
params = {
280-
'timeframe': timeframe,
281-
}
282-
if start_dt is not None:
283-
params['start_dt'] = start_dt
284-
if end_dt is not None:
285-
params['end_dt'] = end_dt
286-
if limit is not None:
287-
params['limit'] = limit
288-
resp = self.get('/assets/{}/bars'.format(symbol), params)
289-
return AssetBars(resp)
246+
if start is not None:
247+
params['start'] = start
248+
if end is not None:
249+
params['end'] = end
250+
if after is not None:
251+
params['after'] = after
252+
if until is not None:
253+
params['until'] = until
254+
resp = self.data_get('/bars/{}'.format(timeframe), params)
255+
return BarSet(resp)
290256

291257
def get_clock(self):
292258
resp = self.get('/clock')

alpaca_trade_api/stream.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re
33
import websocket
44
from .common import get_base_url, get_credentials
5-
from .entity import Account, AssetBars, Quote, Entity
5+
from .entity import Account, Entity
66

77

88
class StreamConn(object):
@@ -55,10 +55,6 @@ def run(self):
5555
def _cast(self, stream, msg):
5656
if stream == 'account_updates':
5757
return Account(msg)
58-
elif re.match(r'^bars/', stream):
59-
return AssetBars(msg)
60-
elif re.match(r'^quotes/', stream):
61-
return Quote(msg)
6258
return Entity(msg)
6359

6460
def _dispatch(self, stream, msg):

alpaca_trade_api/stream2.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
import websockets
55
from .common import get_base_url, get_credentials
6-
from .entity import Account, AssetBars, Quote, Entity
6+
from .entity import Account, Entity
77
from . import polygon
88

99

@@ -109,10 +109,6 @@ async def close(self):
109109
def _cast(self, channel, msg):
110110
if channel == 'account_updates':
111111
return Account(msg)
112-
elif re.match(r'^bars/', channel):
113-
return AssetBars(msg)
114-
elif re.match(r'^quotes/', channel):
115-
return Quote(msg)
116112
return Entity(msg)
117113

118114
async def _dispatch_nats(self, conn, subject, data):

0 commit comments

Comments
 (0)