@@ -434,6 +434,16 @@ def save_ndx(self, **kwargs):
434434 if os .path .exists (filename ):
435435 return filename
436436
437+ def log_api_call_time (self ):
438+ self .last_api_call_time = time ()
439+
440+ def obey_free_limit (self , free_delay ):
441+ if self .free and hasattr (self , 'last_api_call_time' ):
442+ time_since_last_call = time () - self .last_api_call_time
443+ delay = free_delay - time_since_last_call
444+ if delay > 0 :
445+ sleep (delay )
446+
437447
438448class Indices (MarketData ):
439449 def __init__ (self ):
@@ -448,6 +458,102 @@ def get_ndx(self, date=datetime.now()):
448458 return self .standardize_ndx (df )
449459
450460
461+ class Alpaca (MarketData ):
462+ # AlpacaData
463+ def __init__ (
464+ self ,
465+ token = os .environ .get ('ALPACA' ),
466+ secret = os .environ .get ('ALPACA_SECRET' ),
467+ free = True ,
468+ paper = False
469+ ):
470+ super ().__init__ ()
471+ self .base = 'https://data.alpaca.markets'
472+ self .token = os .environ .get (
473+ 'ALPACA_PAPER' ) if paper or C .TEST else token
474+ self .secret = os .environ .get (
475+ 'ALPACA_PAPER_SECRET' ) if paper or C .TEST else secret
476+ if not (self .token and self .secret ):
477+ raise Exception ('missing Alpaca credentials' )
478+ self .provider = 'alpaca'
479+ self .free = free
480+
481+ # def get_dividends(self, **kwargs):
482+ # pass
483+ # def get_splits(self, **kwargs):
484+ # pass
485+
486+ def get_ohlc (self , ** kwargs ):
487+ def _get_ohlc (symbol , timeframe = 'max' ):
488+ is_crypto = symbol in C .ALPC_CRYPTO_SYMBOLS
489+ version = 'v1beta3' if is_crypto else 'v2'
490+ page_token = None
491+ start , _ = self .traveller .convert_dates (timeframe )
492+ parts = [
493+ self .base ,
494+ version ,
495+ 'crypto/us' if is_crypto else 'stocks' ,
496+ 'bars' ,
497+ ]
498+ url = '/' .join (parts )
499+ pre_params = {
500+ 'symbols' : symbol ,
501+ 'timeframe' : '1D' ,
502+ 'start' : start ,
503+ 'limit' : 10000 ,
504+ } | ({} if is_crypto else {'adjustment' : 'all' , 'feed' : 'iex' })
505+ headers = {
506+ 'APCA-API-KEY-ID' : self .token ,
507+ 'APCA-API-SECRET-KEY' : self .secret
508+ }
509+ results = []
510+ while True :
511+ self .obey_free_limit (C .ALPACA_FREE_DELAY )
512+ try :
513+ post_params = {
514+ 'page_token' : page_token } if page_token else {}
515+ params = pre_params | post_params
516+ response = requests .get (url , params , headers = headers )
517+ if not response .ok :
518+ raise Exception (
519+ 'Invalid response from Alpaca for OHLC' ,
520+ response .status_code ,
521+ response .text
522+ )
523+ data = response .json ()
524+ if data .get ('bars' ) and data ['bars' ].get (symbol ):
525+ results += data ['bars' ][symbol ]
526+ finally :
527+ self .log_api_call_time ()
528+ if data .get ('next_page_token' ):
529+ page_token = data ['next_page_token' ]
530+ else :
531+ break
532+ df = pd .DataFrame (results )
533+ columns = {
534+ 't' : 'date' ,
535+ 'o' : 'open' ,
536+ 'h' : 'high' ,
537+ 'l' : 'low' ,
538+ 'c' : 'close' ,
539+ 'v' : 'volume' ,
540+ 'vw' : 'average' ,
541+ 'n' : 'trades'
542+ }
543+ df = df .rename (columns = columns )
544+ df ['date' ] = pd .to_datetime (df ['date' ]).dt .tz_convert (
545+ C .TZ ).dt .tz_localize (None )
546+ df = self .standardize_ohlc (symbol , df )
547+ return self .reader .data_in_timeframe (df , C .TIME , timeframe )
548+ return self .try_again (func = _get_ohlc , ** kwargs )
549+
550+ # def get_intraday(self, **kwargs):
551+ # pass
552+
553+ # def get_news(self, **kwargs):
554+ # pass
555+
556+
451557class Polygon (MarketData ):
452558 def __init__ (self , token = os .environ .get ('POLYGON' ), free = True ):
453559 super ().__init__ ()
@@ -465,19 +571,9 @@ def paginate(self, gen, apply):
465571 results .append (apply (item ))
466572 return results
467573
468- def obey_free_limit (self ):
469- if self .free and hasattr (self , 'last_api_call_time' ):
470- time_since_last_call = time () - self .last_api_call_time
471- delay = C .POLY_FREE_DELAY - time_since_last_call
472- if delay > 0 :
473- sleep (delay )
474-
475- def log_api_call_time (self ):
476- self .last_api_call_time = time ()
477-
478574 def get_dividends (self , ** kwargs ):
479575 def _get_dividends (symbol , timeframe = 'max' ):
480- self .obey_free_limit ()
576+ self .obey_free_limit (C . POLY_FREE_DELAY )
481577 try :
482578 start , _ = self .traveller .convert_dates (timeframe )
483579 response = self .paginate (
@@ -495,8 +591,6 @@ def _get_dividends(symbol, timeframe='max'):
495591 'amount' : div .cash_amount
496592 }
497593 )
498- except Exception as e :
499- raise e
500594 finally :
501595 self .log_api_call_time ()
502596 raw = pd .DataFrame (response )
@@ -506,7 +600,7 @@ def _get_dividends(symbol, timeframe='max'):
506600
507601 def get_splits (self , ** kwargs ):
508602 def _get_splits (symbol , timeframe = 'max' ):
509- self .obey_free_limit ()
603+ self .obey_free_limit (C . POLY_FREE_DELAY )
510604 try :
511605 start , _ = self .traveller .convert_dates (timeframe )
512606 response = self .paginate (
@@ -522,8 +616,6 @@ def _get_splits(symbol, timeframe='max'):
522616 'ratio' : split .split_from / split .split_to
523617 }
524618 )
525- except Exception as e :
526- raise e
527619 finally :
528620 self .log_api_call_time ()
529621 raw = pd .DataFrame (response )
@@ -536,14 +628,12 @@ def _get_ohlc(symbol, timeframe='max'):
536628 is_crypto = symbol .find ('X%3A' ) == 0
537629 formatted_start , formatted_end = self .traveller .convert_dates (
538630 timeframe )
539- self .obey_free_limit ()
631+ self .obey_free_limit (C . POLY_FREE_DELAY )
540632 try :
541633 response = self .client .get_aggs (
542634 symbol , 1 , 'day' ,
543635 from_ = formatted_start , to = formatted_end , adjusted = True , limit = C .POLY_MAX_AGGS_LIMIT
544636 )
545- except Exception as e :
546- raise e
547637 finally :
548638 self .log_api_call_time ()
549639
@@ -573,7 +663,7 @@ def _get_intraday(symbol, min=1, timeframe='max', extra_hrs=True):
573663 raise Exception (f'No dates in timeframe: { timeframe } .' )
574664
575665 for _ , date in enumerate (dates ):
576- self .obey_free_limit ()
666+ self .obey_free_limit (C . POLY_FREE_DELAY )
577667 try :
578668 response = self .client .get_aggs (
579669 symbol , min , 'minute' , from_ = date , to = date ,
@@ -582,8 +672,6 @@ def _get_intraday(symbol, min=1, timeframe='max', extra_hrs=True):
582672 except exceptions .NoResultsError :
583673 # This is to prevent breaking the loop over weekends
584674 continue
585- except Exception as e :
586- raise e
587675 finally :
588676 self .log_api_call_time ()
589677
0 commit comments