Skip to content

Commit 9c78d41

Browse files
authored
Add snapshot endpoints (#426)
* Add snapshot endpoints * fix flake8 errors * fix return type and docstring for snapshots * update example
1 parent 1993392 commit 9c78d41

File tree

3 files changed

+239
-20
lines changed

3 files changed

+239
-20
lines changed

alpaca_trade_api/entity_v2.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ def df(self):
6767

6868

6969
class Remapped:
70-
def __init__(self, mapping: Dict[str,str], *args, **kwargs):
71-
self._reversed_mapping = {value: key for (key, value) in mapping.items()}
70+
def __init__(self, mapping: Dict[str, str], *args, **kwargs):
71+
self._reversed_mapping = {
72+
value: key for (key, value) in mapping.items()}
7273
super().__init__(*args, **kwargs)
73-
74+
7475
def __getattr__(self, key):
7576
if key in self._reversed_mapping:
7677
return super().__getattr__(self._reversed_mapping[key])
@@ -102,5 +103,33 @@ def __init__(self, raw):
102103
class QuoteV2(Remapped, _NanoTimestamped, Entity):
103104
_tskeys = ('t',)
104105

105-
def __init__(self,raw):
106+
def __init__(self, raw):
106107
super().__init__(quote_mapping_v2, raw)
108+
109+
110+
class BarV2(Remapped, _NanoTimestamped, Entity):
111+
_tskeys = ('t',)
112+
113+
def __init__(self, raw):
114+
super().__init__(bar_mapping_v2, raw)
115+
116+
117+
class SnapshotV2:
118+
def __init__(self, raw):
119+
self.latest_trade = _convert_or_none(TradeV2, raw.get('latestTrade'))
120+
self.latest_quote = _convert_or_none(QuoteV2, raw.get('latestQuote'))
121+
self.minute_bar = _convert_or_none(BarV2, raw.get('minuteBar'))
122+
self.daily_bar = _convert_or_none(BarV2, raw.get('dailyBar'))
123+
self.prev_daily_bar = _convert_or_none(BarV2, raw.get('prevDailyBar'))
124+
125+
126+
class SnapshotsV2(dict):
127+
def __init__(self, raw):
128+
for k, v in raw.items():
129+
self[k] = _convert_or_none(SnapshotV2, v)
130+
131+
132+
def _convert_or_none(entityType, value):
133+
if value:
134+
return entityType(value)
135+
return None

alpaca_trade_api/rest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
Asset, Order, Position, BarSet, Clock, Calendar,
1717
Aggs, Trade, Quote, Watchlist, PortfolioHistory
1818
)
19-
from .entity_v2 import BarsV2, TradesV2, TradeV2, QuotesV2, QuoteV2
19+
from .entity_v2 import (
20+
BarsV2, SnapshotV2, SnapshotsV2, TradesV2, TradeV2, QuotesV2, QuoteV2)
2021
from . import polygon
2122

2223
logger = logging.getLogger(__name__)
@@ -652,6 +653,19 @@ def get_latest_quote(self, symbol: str) -> QuoteV2:
652653
api_version='v2')
653654
return self.response_wrapper(resp['quote'], QuoteV2)
654655

656+
def get_snapshot(self, symbol: str) -> SnapshotV2:
657+
"""Get the snapshot for the given symbol"""
658+
resp = self.data_get('/stocks/{}/snapshot'.format(symbol),
659+
api_version='v2')
660+
return self.response_wrapper(resp, SnapshotV2)
661+
662+
def get_snapshots(self, symbols: List[str]) -> SnapshotsV2:
663+
"""Get the snapshots for the given symbols"""
664+
resp = self.data_get(
665+
'/stocks/snapshots?symbols={}'.format(','.join(symbols)),
666+
api_version='v2')
667+
return self.response_wrapper(resp, SnapshotsV2)
668+
655669
def get_clock(self) -> Clock:
656670
resp = self.get('/clock')
657671
return self.response_wrapper(resp, Clock)

tests/test_rest.py

Lines changed: 191 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -546,21 +546,21 @@ def test_data(reqmock):
546546
'https://data.alpaca.markets/v2/stocks/AAPL/trades/latest',
547547
text='''
548548
{
549-
"symbol": "AAPL",
550-
"trade": {
551-
"t": "2021-04-20T12:40:34.123456789Z",
552-
"x": "J",
553-
"p": 134.7,
554-
"s": 20,
555-
"c": [
556-
"@",
557-
"T",
558-
"I"
559-
],
560-
"i": 32,
561-
"z": "C"
562-
}
563-
}
549+
"symbol": "AAPL",
550+
"trade": {
551+
"t": "2021-04-20T12:40:34.123456789Z",
552+
"x": "J",
553+
"p": 134.7,
554+
"s": 20,
555+
"c": [
556+
"@",
557+
"T",
558+
"I"
559+
],
560+
"i": 32,
561+
"z": "C"
562+
}
563+
}
564564
'''
565565
)
566566
latest_trade = api.get_latest_trade('AAPL')
@@ -612,6 +612,182 @@ def test_data(reqmock):
612612
assert type(latest_quote) == tradeapi.entity_v2.QuoteV2
613613
assert type(api_raw.get_latest_quote('AAPL')) == dict
614614

615+
# Snapshot
616+
reqmock.get(
617+
'https://data.alpaca.markets/v2/stocks/AAPL/snapshot',
618+
text='''
619+
{
620+
"symbol": "AAPL",
621+
"latestTrade": {
622+
"t": "2021-05-03T14:45:50.456Z",
623+
"x": "D",
624+
"p": 133.55,
625+
"s": 200,
626+
"c": [
627+
"@"
628+
],
629+
"i": 61462,
630+
"z": "C"
631+
},
632+
"latestQuote": {
633+
"t": "2021-05-03T14:45:50.532316972Z",
634+
"ax": "P",
635+
"ap": 133.55,
636+
"as": 7,
637+
"bx": "Q",
638+
"bp": 133.54,
639+
"bs": 9,
640+
"c": [
641+
"R"
642+
]
643+
},
644+
"minuteBar": {
645+
"t": "2021-05-03T14:44:00Z",
646+
"o": 133.485,
647+
"h": 133.4939,
648+
"l": 133.42,
649+
"c": 133.445,
650+
"v": 182818
651+
},
652+
"dailyBar": {
653+
"t": "2021-05-03T04:00:00Z",
654+
"o": 132.04,
655+
"h": 134.07,
656+
"l": 131.83,
657+
"c": 133.445,
658+
"v": 25094213
659+
},
660+
"prevDailyBar": {
661+
"t": "2021-04-30T04:00:00Z",
662+
"o": 131.82,
663+
"h": 133.56,
664+
"l": 131.065,
665+
"c": 131.46,
666+
"v": 109506363
667+
}
668+
}'''
669+
)
670+
snapshot = api.get_snapshot('AAPL')
671+
assert snapshot.latest_trade.price == 133.55
672+
assert snapshot.latest_quote.bid_size == 9
673+
assert snapshot.minute_bar.open == 133.485
674+
assert snapshot.daily_bar.high == 134.07
675+
assert snapshot.prev_daily_bar.volume == 109506363
676+
677+
# Snapshots
678+
reqmock.get(
679+
'https://data.alpaca.markets/v2/stocks/snapshots' +
680+
'?symbols=AAPL,MSFT,INVALID',
681+
text='''
682+
{
683+
"AAPL": {
684+
"latestTrade": {
685+
"t": "2021-05-03T14:48:06.563Z",
686+
"x": "D",
687+
"p": 133.4201,
688+
"s": 145,
689+
"c": [
690+
"@"
691+
],
692+
"i": 62700,
693+
"z": "C"
694+
},
695+
"latestQuote": {
696+
"t": "2021-05-03T14:48:07.257820915Z",
697+
"ax": "Q",
698+
"ap": 133.43,
699+
"as": 7,
700+
"bx": "Q",
701+
"bp": 133.42,
702+
"bs": 15,
703+
"c": [
704+
"R"
705+
]
706+
},
707+
"minuteBar": {
708+
"t": "2021-05-03T14:47:00Z",
709+
"o": 133.4401,
710+
"h": 133.48,
711+
"l": 133.37,
712+
"c": 133.42,
713+
"v": 207020
714+
},
715+
"dailyBar": {
716+
"t": "2021-05-03T04:00:00Z",
717+
"o": 132.04,
718+
"h": 134.07,
719+
"l": 131.83,
720+
"c": 133.42,
721+
"v": 25846800
722+
},
723+
"prevDailyBar": {
724+
"t": "2021-04-30T04:00:00Z",
725+
"o": 131.82,
726+
"h": 133.56,
727+
"l": 131.065,
728+
"c": 131.46,
729+
"v": 109506363
730+
}
731+
},
732+
"MSFT": {
733+
"latestTrade": {
734+
"t": "2021-05-03T14:48:06.36Z",
735+
"x": "D",
736+
"p": 253.8738,
737+
"s": 100,
738+
"c": [
739+
"@"
740+
],
741+
"i": 22973,
742+
"z": "C"
743+
},
744+
"latestQuote": {
745+
"t": "2021-05-03T14:48:07.243353456Z",
746+
"ax": "N",
747+
"ap": 253.89,
748+
"as": 2,
749+
"bx": "Q",
750+
"bp": 253.87,
751+
"bs": 2,
752+
"c": [
753+
"R"
754+
]
755+
},
756+
"minuteBar": {
757+
"t": "2021-05-03T14:47:00Z",
758+
"o": 253.78,
759+
"h": 253.869,
760+
"l": 253.78,
761+
"c": 253.855,
762+
"v": 25717
763+
},
764+
"dailyBar": {
765+
"t": "2021-05-03T04:00:00Z",
766+
"o": 253.34,
767+
"h": 254.35,
768+
"l": 251.8,
769+
"c": 253.855,
770+
"v": 6100459
771+
},
772+
"prevDailyBar": null
773+
},
774+
"INVALID": null
775+
}
776+
'''
777+
)
778+
snapshots = api.get_snapshots(['AAPL', 'MSFT', 'INVALID'])
779+
assert len(snapshots) == 3
780+
aapl_snapshot = snapshots.get('AAPL')
781+
assert aapl_snapshot is not None
782+
assert aapl_snapshot.latest_trade.size == 145
783+
assert aapl_snapshot.latest_quote.bid_exchange == "Q"
784+
msft_snapshot = snapshots.get('MSFT')
785+
assert msft_snapshot is not None
786+
assert msft_snapshot.minute_bar.low == 253.78
787+
assert msft_snapshot.daily_bar.close == 253.855
788+
assert msft_snapshot.prev_daily_bar is None
789+
assert snapshots.get('INVALID') is None
790+
615791

616792
def test_watchlists(reqmock):
617793
api = tradeapi.REST('key-id', 'secret-key', api_version='v1')

0 commit comments

Comments
 (0)