Skip to content

Commit 1ddac28

Browse files
committed
Add accessor for new bars data endpoint
Fix flake8 error Fix reqmock address in new unit test Replace apostrophes with quote marks in new test JSON Remove older data entities Remove older data entities Remove test script Add override and environment configuration for base URL Make BarSet a map of string to Bar Fix an indentation error Add automatic conversion of t on Bar entities to timestamp Rename get_bars to get_barset, Bars.df PEP8 Update readme with bars endpoint info
1 parent 1b30063 commit 1ddac28

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,
@@ -83,7 +87,7 @@ def _request(self, method, path, data=None, prefix='/v1'):
8387
return self._one_request(method, url, opts, retry)
8488
except RetryException:
8589
retry_wait = self._retry_wait
86-
logger.warn(
90+
logger.warning(
8791
'sleep {} seconds and retrying {} '
8892
'{} more time(s)...'.format(
8993
retry_wait, url, retry))
@@ -125,6 +129,10 @@ def post(self, path, data=None):
125129
def delete(self, path, data=None):
126130
return self._request('DELETE', path, data)
127131

132+
def data_get(self, path, data=None):
133+
base_url = get_data_url()
134+
return self._request('GET', path, data, base_url=base_url)
135+
128136
def get_account(self):
129137
'''Get the account'''
130138
resp = self.get('/account')
@@ -210,78 +218,36 @@ def get_asset(self, symbol):
210218
resp = self.get('/assets/{}'.format(symbol))
211219
return Asset(resp)
212220

213-
def list_quotes(self, symbols):
214-
'''Get a list of quotes'''
215-
if not isinstance(symbols, str):
216-
symbols = ','.join(symbols)
217-
params = {
218-
'symbols': symbols,
219-
}
220-
resp = self.get('/quotes', params)
221-
return [Quote(o) for o in resp]
222-
223-
def get_quote(self, symbol):
224-
'''Get a quote'''
225-
resp = self.get('/assets/{}/quote'.format(symbol))
226-
return Quote(resp)
227-
228-
def list_fundamentals(self, symbols):
229-
'''Get a list of fundamentals'''
230-
if not isinstance(symbols, str):
231-
symbols = ','.join(symbols)
232-
params = {
233-
'symbols': symbols,
234-
}
235-
resp = self.get('/fundamentals', params)
236-
return [Fundamental(o) for o in resp]
237-
238-
def get_fundamental(self, symbol):
239-
'''Get a fundamental'''
240-
resp = self.get('/assets/{}/fundamental'.format(symbol))
241-
return Fundamental(resp)
242-
243-
def list_bars(
244-
self,
245-
symbols,
246-
timeframe,
247-
start_dt=None,
248-
end_dt=None,
249-
limit=None):
250-
'''Get a list of bars'''
221+
def get_barset(self,
222+
symbols,
223+
timeframe,
224+
limit=None,
225+
start=None,
226+
end=None,
227+
after=None,
228+
until=None):
229+
'''Get BarSet(dict[str]->list[Bar])
230+
The parameter symbols can be either a comma-split string
231+
or a list of string. Each symbol becomes the key of
232+
the returned value.
233+
'''
251234
if not isinstance(symbols, str):
252235
symbols = ','.join(symbols)
253236
params = {
254237
'symbols': symbols,
255-
'timeframe': timeframe,
256238
}
257-
if start_dt is not None:
258-
params['start_dt'] = start_dt
259-
if end_dt is not None:
260-
params['end_dt'] = end_dt
261239
if limit is not None:
262240
params['limit'] = limit
263-
resp = self.get('/bars', params)
264-
return [AssetBars(o) for o in resp]
265-
266-
def get_bars(
267-
self,
268-
symbol,
269-
timeframe,
270-
start_dt=None,
271-
end_dt=None,
272-
limit=None):
273-
'''Get bars'''
274-
params = {
275-
'timeframe': timeframe,
276-
}
277-
if start_dt is not None:
278-
params['start_dt'] = start_dt
279-
if end_dt is not None:
280-
params['end_dt'] = end_dt
281-
if limit is not None:
282-
params['limit'] = limit
283-
resp = self.get('/assets/{}/bars'.format(symbol), params)
284-
return AssetBars(resp)
241+
if start is not None:
242+
params['start'] = start
243+
if end is not None:
244+
params['end'] = end
245+
if after is not None:
246+
params['after'] = after
247+
if until is not None:
248+
params['until'] = until
249+
resp = self.data_get('/bars/{}'.format(timeframe), params)
250+
return BarSet(resp)
285251

286252
def get_clock(self):
287253
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)