Skip to content

Commit 5dcbddd

Browse files
committed
feature:: support minute aggs as streaming source
so far the only data source was quotes (bid ask) with this addition one can select the data source to be: - quotes - minute aggs
1 parent 2a7a14a commit 5dcbddd

File tree

2 files changed

+70
-14
lines changed

2 files changed

+70
-14
lines changed

alpaca_backtrader_api/alpacadata.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from backtrader.feed import DataBase
77
from backtrader import date2num, num2date
88
from backtrader.utils.py3 import queue, with_metaclass
9+
import backtrader as bt
910

1011
from alpaca_backtrader_api import alpacastore
1112

@@ -155,7 +156,13 @@ def islive(self):
155156
def __init__(self, **kwargs):
156157
self.o = self._store(**kwargs)
157158
self._candleFormat = 'bidask' if self.p.bidask else 'midpoint'
159+
self._timeframe = self.p.timeframe
158160
self.do_qcheck(True, 0)
161+
if self._timeframe not in [bt.TimeFrame.Ticks,
162+
bt.TimeFrame.Minutes,
163+
bt.TimeFrame.Days]:
164+
raise Exception(f'Unsupported time frame: '
165+
f'{bt.TimeFrame.TName(self._timeframe)}')
159166

160167
def setenvironment(self, env):
161168
"""
@@ -224,7 +231,9 @@ def _st_start(self, instart=True, tmout=None):
224231

225232
self._state = self._ST_HISTORBACK
226233
return True
227-
self.qlive = self.o.streaming_prices(self.p.dataname, tmout=tmout)
234+
self.qlive = self.o.streaming_prices(self.p.dataname,
235+
self.p.timeframe,
236+
tmout=tmout)
228237
if instart:
229238
self._statelivereconn = self.p.backfill_start
230239
else:
@@ -299,8 +308,13 @@ def _load(self):
299308
if self._laststatus != self.LIVE:
300309
if self.qlive.qsize() <= 1: # very short live queue
301310
self.put_notification(self.LIVE)
302-
303-
ret = self._load_tick(msg)
311+
if self.p.timeframe == bt.TimeFrame.Ticks:
312+
ret = self._load_tick(msg)
313+
elif self.p.timeframe == bt.TimeFrame.Minutes:
314+
ret = self._load_agg(msg)
315+
else:
316+
# might want to act differently in the future
317+
ret = self._load_agg(msg)
304318
if ret:
305319
return True
306320

@@ -410,6 +424,21 @@ def _load_tick(self, msg):
410424

411425
return True
412426

427+
def _load_agg(self, msg):
428+
dtobj = datetime.utcfromtimestamp(int(msg['time']))
429+
dt = date2num(dtobj)
430+
if dt <= self.lines.datetime[-1]:
431+
return False # time already seen
432+
self.lines.datetime[0] = dt
433+
self.lines.open[0] = msg['open']
434+
self.lines.high[0] = msg['high']
435+
self.lines.low[0] = msg['low']
436+
self.lines.close[0] = msg['close']
437+
self.lines.volume[0] = msg['volume']
438+
self.lines.openinterest[0] = 0.0
439+
440+
return True
441+
413442
def _load_history(self, msg):
414443
dtobj = msg['time'].to_pydatetime()
415444
dt = date2num(dtobj)

alpaca_backtrader_api/alpacastore.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
unicode_literals)
33
import os
44
import collections
5+
import time
56
from enum import Enum
67
import traceback
78

@@ -91,10 +92,17 @@ def _request(self,
9192

9293

9394
class Granularity(Enum):
95+
Ticks = "ticks"
9496
Daily = "day"
9597
Minute = "minute"
9698

9799

100+
class StreamingMethod(Enum):
101+
AccountUpdate = 'account_update'
102+
Quote = "quote"
103+
MinuteAgg = "minute_agg"
104+
105+
98106
class Streamer:
99107
conn = None
100108

@@ -104,7 +112,7 @@ def __init__(
104112
api_key='',
105113
api_secret='',
106114
instrument='',
107-
method='',
115+
method: StreamingMethod = StreamingMethod.AccountUpdate,
108116
base_url='',
109117
data_url='',
110118
data_stream='',
@@ -126,19 +134,23 @@ def __init__(
126134
self.q = q
127135
self.conn.on('authenticated')(self.on_auth)
128136
self.conn.on(r'Q.*')(self.on_quotes)
137+
self.conn.on(r'AM.*')(self.on_agg_min)
138+
self.conn.on(r'A.*')(self.on_agg_min)
129139
self.conn.on(r'account_updates')(self.on_account)
130140
self.conn.on(r'trade_updates')(self.on_trade)
131141

132142
def run(self):
133143
channels = []
134-
if not self.method:
144+
if self.method == StreamingMethod.AccountUpdate:
135145
channels = ['trade_updates'] # 'account_updates'
136146
else:
137147
if self.data_stream == 'polygon':
138-
maps = {"quote": "Q."}
148+
maps = {"quote": "Q.",
149+
"minute_agg": "AM."}
139150
elif self.data_stream == 'alpacadatav1':
140-
maps = {"quote": "alpacadatav1/Q."}
141-
channels = [maps[self.method] + self.instrument]
151+
maps = {"quote": "alpacadatav1/Q.",
152+
"minute_agg": "alpacadatav1/AM."}
153+
channels = [maps[self.method.value] + self.instrument]
142154

143155
loop = asyncio.new_event_loop()
144156
asyncio.set_event_loop(loop)
@@ -159,7 +171,8 @@ async def on_agg_sec(self, conn, subject, msg):
159171
self.q.put(msg)
160172

161173
async def on_agg_min(self, conn, subject, msg):
162-
self.q.put(msg)
174+
msg._raw['time'] = msg.end.to_pydatetime().timestamp()
175+
self.q.put(msg._raw)
163176

164177
async def on_account(self, conn, stream, msg):
165178
self.q.put(msg)
@@ -308,6 +321,8 @@ def get_positions(self):
308321
return positions
309322

310323
def get_granularity(self, timeframe, compression) -> Granularity:
324+
if timeframe == bt.TimeFrame.Ticks:
325+
return Granularity.Ticks
311326
if timeframe == bt.TimeFrame.Minutes:
312327
return Granularity.Minute
313328
elif timeframe == bt.TimeFrame.Days:
@@ -440,7 +455,7 @@ def _make_sure_dates_are_initialized_properly(self, dtbegin, dtend,
440455
dates may or may not be specified by the user.
441456
when they do, they are probably don't include NY timezome data
442457
also, when granularity is minute, we want to make sure we get data when
443-
market is opened. so if it doesn't - let's get set end date to be last
458+
market is opened. so if it doesn't - let's set end date to be last
444459
known minute with opened market.
445460
this nethod takes care of all these issues.
446461
:param dtbegin:
@@ -585,9 +600,12 @@ def _iterate_api_calls():
585600
timeframe = "5Min"
586601
elif granularity == 'minute' and compression == 15:
587602
timeframe = "15Min"
603+
elif granularity == 'ticks':
604+
timeframe = "minute"
588605
else:
589606
timeframe = granularity
590607
r = self.oapi.get_barset(dataname,
608+
'minute' if timeframe == 'ticks' else
591609
timeframe,
592610
limit=1000,
593611
end=curr.isoformat()
@@ -687,22 +705,31 @@ def _resample(df):
687705
response = response[~response.index.duplicated()]
688706
return response
689707

690-
def streaming_prices(self, dataname, tmout=None):
708+
def streaming_prices(self, dataname, timeframe, tmout=None):
691709
q = queue.Queue()
692-
kwargs = {'q': q, 'dataname': dataname, 'tmout': tmout}
710+
kwargs = {'q': q,
711+
'dataname': dataname,
712+
'timeframe': timeframe,
713+
'tmout': tmout}
693714
t = threading.Thread(target=self._t_streaming_prices, kwargs=kwargs)
694715
t.daemon = True
695716
t.start()
696717
return q
697718

698-
def _t_streaming_prices(self, dataname, q, tmout):
719+
def _t_streaming_prices(self, dataname, timeframe, q, tmout):
699720
if tmout is not None:
700721
_time.sleep(tmout)
722+
723+
if timeframe == bt.TimeFrame.Ticks:
724+
method = StreamingMethod.Quote
725+
elif timeframe == bt.TimeFrame.Minutes:
726+
method = StreamingMethod.MinuteAgg
727+
701728
streamer = Streamer(q,
702729
api_key=self.p.key_id,
703730
api_secret=self.p.secret_key,
704731
instrument=dataname,
705-
method='quote',
732+
method=method,
706733
base_url=self.p.base_url,
707734
data_url=os.environ.get("DATA_PROXY_WS", ''),
708735
data_stream='polygon' if self.p.usePolygon else

0 commit comments

Comments
 (0)