Skip to content

Commit 23c0c78

Browse files
authored
Merge pull request #384 from alpacahq/feature/data-v2
Data v2
2 parents b7e12fe + f6e965a commit 23c0c78

File tree

13 files changed

+901
-232
lines changed

13 files changed

+901
-232
lines changed

README.md

Lines changed: 176 additions & 185 deletions
Large diffs are not rendered by default.

alpaca_trade_api/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .rest import REST # noqa
2+
from .stream import Stream # noqa
23
from .stream2 import StreamConn # noqa
34

4-
__version__ = '0.53.0'
5+
__version__ = '1.0.0'

alpaca_trade_api/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ def get_data_url() -> URL:
6565
'APCA_API_DATA_URL', 'https://data.alpaca.markets').rstrip('/'))
6666

6767

68+
def get_data_stream_url() -> URL:
69+
return URL(os.environ.get(
70+
'APCA_API_STREAM_URL',
71+
'https://stream.data.alpaca.markets').rstrip('/')
72+
)
73+
74+
6875
def get_credentials(key_id: str = None,
6976
secret_key: str = None,
7077
oauth: str = None) -> Credentials:

alpaca_trade_api/entity_v2.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from enum import Enum
2+
import pandas as pd
3+
from .entity import Bar, Trade, Quote
4+
5+
6+
trade_mapping_v2 = {
7+
"i": "id",
8+
"S": "symbol",
9+
"c": "conditions",
10+
"x": "exchange",
11+
"p": "price",
12+
"s": "size",
13+
"t": "timestamp",
14+
"z": "tape"
15+
}
16+
17+
quote_mapping_v2 = {
18+
"S": "symbol",
19+
"ax": "ask_exchange",
20+
"ap": "ask_price",
21+
"as": "ask_size",
22+
"bx": "bid_exchange",
23+
"bp": "bid_price",
24+
"bs": "bid_size",
25+
"c": "conditions",
26+
"t": "timestamp",
27+
"z": "tape"
28+
}
29+
30+
bar_mapping_v2 = {
31+
"S": "symbol",
32+
"o": "open",
33+
"h": "high",
34+
"l": "low",
35+
"c": "close",
36+
"v": "volume",
37+
"t": "timestamp"
38+
}
39+
40+
41+
class EntityListType(Enum):
42+
Trade = Trade, trade_mapping_v2
43+
Quote = Quote, quote_mapping_v2
44+
Bar = Bar, bar_mapping_v2
45+
46+
47+
class EntityList(list):
48+
def __init__(self, entity_type: EntityListType, raw):
49+
entity = entity_type.value[0]
50+
super().__init__([entity(o) for o in raw])
51+
self._raw = raw
52+
self.mapping = entity_type.value[1]
53+
54+
@property
55+
def df(self):
56+
if not hasattr(self, '_df'):
57+
df = pd.DataFrame(
58+
self._raw,
59+
)
60+
61+
df.columns = [self.mapping[c] for c in df.columns]
62+
if not df.empty:
63+
df.set_index('timestamp', inplace=True)
64+
df.index = pd.DatetimeIndex(df.index)
65+
self._df = df
66+
return self._df
67+
68+
69+
class BarsV2(EntityList):
70+
def __init__(self, raw):
71+
super().__init__(EntityListType.Bar, raw)
72+
73+
74+
class TradesV2(EntityList):
75+
def __init__(self, raw):
76+
super().__init__(EntityListType.Trade, raw)
77+
78+
79+
class QuotesV2(EntityList):
80+
def __init__(self, raw):
81+
super().__init__(EntityListType.Quote, raw)

alpaca_trade_api/rest.py

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import logging
22
import os
3-
from typing import List
3+
from typing import Iterator, List, Union
44
import requests
55
from requests.exceptions import HTTPError
66
import time
7+
from enum import Enum
78
from .common import (
89
get_base_url,
910
get_data_url,
1011
get_credentials,
1112
get_api_version, URL, FLOAT,
1213
)
1314
from .entity import (
14-
Entity, Account, AccountConfigurations, AccountActivity,
15+
Bar, Entity, Account, AccountConfigurations, AccountActivity,
1516
Asset, Order, Position, BarSet, Clock, Calendar,
1617
Aggs, Trade, Quote, Watchlist, PortfolioHistory
1718
)
19+
from .entity_v2 import BarsV2, TradesV2, QuotesV2
1820
from . import polygon
1921

2022
logger = logging.getLogger(__name__)
@@ -24,6 +26,11 @@
2426
AccountActivities = List[AccountActivity]
2527
Calendars = List[Calendar]
2628
Watchlists = List[Watchlist]
29+
TradeIterator = Iterator[Union[Trade, dict]]
30+
QuoteIterator = Iterator[Union[Quote, dict]]
31+
BarIterator = Iterator[Union[Bar, dict]]
32+
33+
DATA_V2_MAX_LIMIT = 10000 # max items per api call
2734

2835

2936
class RetryException(Exception):
@@ -62,6 +69,13 @@ def response(self):
6269
return self._http_error.response
6370

6471

72+
class TimeFrame(Enum):
73+
Day = "1Day"
74+
Hour = "1Hour"
75+
Minute = "1Min"
76+
Sec = "1Sec"
77+
78+
6579
class REST(object):
6680
def __init__(self,
6781
key_id: str = None,
@@ -173,10 +187,10 @@ def patch(self, path, data=None):
173187
def delete(self, path, data=None):
174188
return self._request('DELETE', path, data)
175189

176-
def data_get(self, path, data=None):
190+
def data_get(self, path, data=None, api_version='v1'):
177191
base_url: URL = get_data_url()
178192
return self._request(
179-
'GET', path, data, base_url=base_url, api_version='v1'
193+
'GET', path, data, base_url=base_url, api_version=api_version,
180194
)
181195

182196
def get_account(self) -> Account:
@@ -505,6 +519,118 @@ def get_last_quote(self, symbol: str) -> Quote:
505519
resp = self.data_get('/last_quote/stocks/{}'.format(symbol))
506520
return self.response_wrapper(resp['last'], Quote)
507521

522+
def _data_get_v2(self, endpoint: str, symbol: str, **kwargs):
523+
page_token = None
524+
total_items = 0
525+
limit = kwargs.get('limit')
526+
while True:
527+
actual_limit = None
528+
if limit:
529+
actual_limit = min(int(limit) - total_items, DATA_V2_MAX_LIMIT)
530+
if actual_limit < 1:
531+
break
532+
data = kwargs
533+
data['limit'] = actual_limit
534+
data['page_token'] = page_token
535+
resp = self.data_get('/stocks/{}/{}'.format(symbol, endpoint),
536+
data=data, api_version='v2')
537+
items = resp.get(endpoint, [])
538+
for item in items:
539+
yield item
540+
total_items += len(items)
541+
page_token = resp.get('next_page_token')
542+
if not page_token:
543+
break
544+
545+
def get_trades_iter(self,
546+
symbol: str,
547+
start: str,
548+
end: str,
549+
limit: int = None,
550+
raw=False) -> TradeIterator:
551+
trades = self._data_get_v2('trades', symbol,
552+
start=start, end=end, limit=limit)
553+
for trade in trades:
554+
if raw:
555+
yield trade
556+
else:
557+
yield self.response_wrapper(trade, Trade)
558+
559+
def get_trades(self,
560+
symbol: str,
561+
start: str,
562+
end: str,
563+
limit: int = None,
564+
) -> TradesV2:
565+
trades = list(self.get_trades_iter(symbol,
566+
start,
567+
end,
568+
limit,
569+
raw=True))
570+
return TradesV2(trades)
571+
572+
def get_quotes_iter(self,
573+
symbol: str,
574+
start: str,
575+
end: str,
576+
limit: int = None,
577+
raw=False) -> QuoteIterator:
578+
quotes = self._data_get_v2('quotes', symbol,
579+
start=start, end=end, limit=limit)
580+
for quote in quotes:
581+
if raw:
582+
yield quote
583+
else:
584+
yield self.response_wrapper(quote, Quote)
585+
586+
def get_quotes(self,
587+
symbol: str,
588+
start: str,
589+
end: str,
590+
limit: int = None,
591+
) -> QuotesV2:
592+
quotes = list(self.get_quotes_iter(symbol,
593+
start,
594+
end,
595+
limit,
596+
raw=True))
597+
return QuotesV2(quotes)
598+
599+
def get_bars_iter(self,
600+
symbol: str,
601+
timeframe: TimeFrame,
602+
start: str,
603+
end: str,
604+
adjustment: str = 'all',
605+
limit: int = None,
606+
raw=False) -> BarIterator:
607+
bars = self._data_get_v2('bars', symbol,
608+
timeframe=timeframe.value,
609+
adjustment=adjustment,
610+
start=start, end=end, limit=limit)
611+
for bar in bars:
612+
if raw:
613+
yield bar
614+
else:
615+
yield self.response_wrapper(bar, Bar)
616+
617+
def get_bars(self,
618+
symbol: str,
619+
timeframe: TimeFrame,
620+
start: str,
621+
end: str,
622+
adjustment: str = 'all',
623+
limit: int = None,
624+
) -> BarsV2:
625+
bars = list(self.get_bars_iter(symbol,
626+
timeframe,
627+
start,
628+
end,
629+
adjustment,
630+
limit,
631+
raw=True))
632+
return BarsV2(bars)
633+
508634
def get_clock(self) -> Clock:
509635
resp = self.get('/clock')
510636
return self.response_wrapper(resp, Clock)

0 commit comments

Comments
 (0)