├── .gitignore ├── README.md ├── backtest.py ├── populate_stock_minute_table.py ├── qqq.csv └── stock_price_minute_table.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fullstack-trading-app 2 | A full stack Python app for trading using the Alpaca API 3 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | import config 2 | import backtrader, pandas, sqlite3 3 | from datetime import date, datetime, time, timedelta 4 | 5 | class OpeningRangeBreakout(backtrader.Strategy): 6 | params = dict( 7 | num_opening_bars=15 8 | ) 9 | 10 | def __init__(self): 11 | self.opening_range_low = 0 12 | self.opening_range_high = 0 13 | self.opening_range = 0 14 | self.bought_today = False 15 | self.order = None 16 | 17 | def log(self, txt, dt=None): 18 | if dt is None: 19 | dt = self.datas[0].datetime.datetime() 20 | 21 | print('%s, %s' % (dt, txt)) 22 | 23 | def notify_order(self, order): 24 | if order.status in [order.Submitted, order.Accepted]: 25 | # Buy/Sell order submitted/accepted to/by broker - Nothing to do 26 | return 27 | 28 | # Check if an order has been completed 29 | if order.status in [order.Completed]: 30 | order_details = f"{order.executed.price}, Cost: {order.executed.value}, Comm {order.executed.comm}" 31 | 32 | if order.isbuy(): 33 | self.log(f"BUY EXECUTED, Price: {order_details}") 34 | else: # Sell 35 | self.log(f"SELL EXECUTED, Price: {order_details}") 36 | 37 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 38 | self.log('Order Canceled/Margin/Rejected') 39 | 40 | self.order = None 41 | 42 | def next(self): 43 | current_bar_datetime = self.data.num2date(self.data.datetime[0]) 44 | previous_bar_datetime = self.data.num2date(self.data.datetime[-1]) 45 | 46 | if current_bar_datetime.date() != previous_bar_datetime.date(): 47 | self.opening_range_low = self.data.low[0] 48 | self.opening_range_high = self.data.high[0] 49 | self.bought_today = False 50 | 51 | opening_range_start_time = time(9, 30, 0) 52 | dt = datetime.combine(date.today(), opening_range_start_time) + timedelta(minutes=self.p.num_opening_bars) 53 | opening_range_end_time = dt.time() 54 | 55 | if current_bar_datetime.time() >= opening_range_start_time \ 56 | and current_bar_datetime.time() < opening_range_end_time: 57 | self.opening_range_high = max(self.data.high[0], self.opening_range_high) 58 | self.opening_range_low = min(self.data.low[0], self.opening_range_low) 59 | self.opening_range = self.opening_range_high - self.opening_range_low 60 | else: 61 | if self.order: 62 | return 63 | 64 | if self.position and (self.data.close[0] > (self.opening_range_high + self.opening_range)): 65 | self.close() 66 | 67 | if self.data.close[0] > self.opening_range_high and not self.position and not self.bought_today: 68 | self.order = self.buy() 69 | self.bought_today = True 70 | 71 | if self.position and (self.data.close[0] < (self.opening_range_high - self.opening_range)): 72 | self.order = self.close() 73 | 74 | if self.position and current_bar_datetime.time() >= time(15, 45, 0): 75 | self.log("RUNNING OUT OF TIME - LIQUIDATING POSITION") 76 | self.close() 77 | 78 | def stop(self): 79 | self.log('(Num Opening Bars %2d) Ending Value %.2f' % 80 | (self.params.num_opening_bars, self.broker.getvalue())) 81 | 82 | if self.broker.getvalue() > 130000: 83 | self.log("*** BIG WINNER ***") 84 | 85 | if self.broker.getvalue() < 70000: 86 | self.log("*** MAJOR LOSER ***") 87 | 88 | if __name__ == '__main__': 89 | conn = sqlite3.connect(config.DB_FILE) 90 | conn.row_factory = sqlite3.Row 91 | cursor = conn.cursor() 92 | cursor.execute(""" 93 | SELECT DISTINCT(stock_id) as stock_id FROM stock_price_minute 94 | """) 95 | stocks = cursor.fetchall() 96 | for stock in stocks: 97 | print(f"== Testing {stock['stock_id']} ==") 98 | 99 | cerebro = backtrader.Cerebro() 100 | cerebro.broker.setcash(100000.0) 101 | cerebro.addsizer(backtrader.sizers.PercentSizer, percents=95) 102 | 103 | dataframe = pandas.read_sql(""" 104 | select datetime, open, high, low, close, volume 105 | from stock_price_minute 106 | where stock_id = :stock_id 107 | and strftime('%H:%M:%S', datetime) >= '09:30:00' 108 | and strftime('%H:%M:%S', datetime) < '16:00:00' 109 | order by datetime asc 110 | """, conn, params={"stock_id": stock['stock_id']}, index_col='datetime', parse_dates=['datetime']) 111 | 112 | data = backtrader.feeds.PandasData(dataname=dataframe) 113 | 114 | cerebro.adddata(data) 115 | cerebro.addstrategy(OpeningRangeBreakout) 116 | 117 | #strats = cerebro.optstrategy(OpeningRangeBreakout, num_opening_bars=[15, 30, 60]) 118 | 119 | cerebro.run() 120 | #cerebro.plot() 121 | -------------------------------------------------------------------------------- /populate_stock_minute_table.py: -------------------------------------------------------------------------------- 1 | import config 2 | import sqlite3 3 | import pandas 4 | import csv 5 | import alpaca_trade_api as tradeapi 6 | from datetime import datetime, timedelta 7 | 8 | connection = sqlite3.connect(config.DB_FILE) 9 | connection.row_factory = sqlite3.Row 10 | 11 | cursor = connection.cursor() 12 | 13 | api = tradeapi.REST(config.API_KEY, config.SECRET_KEY, config.API_URL) 14 | symbols = [] 15 | stock_ids = {} 16 | 17 | with open('qqq.csv') as f: 18 | reader = csv.reader(f) 19 | 20 | for line in reader: 21 | symbols.append(line[1]) 22 | 23 | cursor.execute(""" 24 | SELECT * FROM stock 25 | """) 26 | 27 | stocks = cursor.fetchall() 28 | 29 | for stock in stocks: 30 | symbol = stock['symbol'] 31 | stock_ids[symbol] = stock['id'] 32 | 33 | for symbol in symbols: 34 | start_date = datetime(2020, 1, 6).date() 35 | end_date_range = datetime(2020, 11, 20).date() 36 | 37 | while start_date < end_date_range: 38 | end_date = start_date + timedelta(days=4) 39 | 40 | print(f"== Fetching minute bars for {symbol} {start_date} - {end_date} ==") 41 | minutes = api.polygon.historic_agg_v2(symbol, 1, 'minute', _from=start_date, to=end_date).df 42 | minutes = minutes.resample('1min').ffill() 43 | 44 | for index, row in minutes.iterrows(): 45 | cursor.execute(""" 46 | INSERT INTO stock_price_minute (stock_id, datetime, open, high, low, close, volume) 47 | VALUES (?, ?, ?, ?, ?, ?, ?) 48 | """, (stock_ids[symbol], index.tz_localize(None).isoformat(), row['open'], row['high'], row['low'], row['close'], row['volume'])) 49 | 50 | start_date = start_date + timedelta(days=7) 51 | 52 | connection.commit() 53 | -------------------------------------------------------------------------------- /qqq.csv: -------------------------------------------------------------------------------- 1 | Activision Blizzard,ATVI 2 | Adobe Inc.,ADBE 3 | Advanced Micro Devices,AMD 4 | Alexion Pharmaceuticals,ALXN 5 | Align Technology,ALGN 6 | Alphabet Inc. (Class A),GOOGL 7 | Alphabet Inc. (Class C),GOOG 8 | Amazon.com,AMZN 9 | Amgen,AMGN 10 | Analog Devices,ADI 11 | ANSYS,ANSS 12 | Apple Inc.,AAPL 13 | "Applied Materials, Inc.",AMAT 14 | ASML Holding,ASML 15 | Autodesk,ADSK 16 | "Automatic Data Processing, Inc.",ADP 17 | Baidu,BIDU 18 | Biogen,BIIB 19 | BioMarin Pharmaceutical Inc.,BMRN 20 | Booking Holdings,BKNG 21 | Broadcom Inc.,AVGO 22 | Cadence Design Systems,CDNS 23 | CDW,CDW 24 | Cerner Corporation,CERN 25 | "Charter Communications, Inc.",CHTR 26 | Check Point Software Technologies Ltd.,CHKP 27 | Cintas Corporation,CTAS 28 | Cisco Systems,CSCO 29 | Citrix Systems,CTXS 30 | Cognizant Technology Solutions Corporation,CTSH 31 | Comcast Corporation,CMCSA 32 | Copart,CPRT 33 | Costco Wholesale Corporation,COST 34 | CSX Corporation,CSX 35 | DexCom,DXCM 36 | DocuSign,DOCU 37 | "Dollar Tree, Inc.",DLTR 38 | eBay Inc.,EBAY 39 | Electronic Arts,EA 40 | Exelon Corporation,EXC 41 | Expedia Group,EXPE 42 | "Facebook, Inc.",FB 43 | Fastenal Company,FAST 44 | "Fiserv, Inc.",FISV 45 | Fox Corporation (Class A),FOXA 46 | Fox Corporation (Class B),FOX 47 | "Gilead Sciences, Inc.",GILD 48 | IDEXX Laboratories,IDXX 49 | "Illumina, Inc.",ILMN 50 | Incyte Corporation,INCY 51 | Intel Corporation,INTC 52 | Intuit,INTU 53 | Intuitive Surgical,ISRG 54 | JD.com,JD 55 | Keurig Dr Pepper Inc.,KDP 56 | KLA Corporation,KLAC 57 | Kraft Heinz Co,KHC 58 | Lam Research,LRCX 59 | Liberty Global (Class A),LBTYA 60 | Liberty Global (Class C),LBTYK 61 | Lululemon Athletica,LULU 62 | Marriott International,MAR 63 | Maxim Integrated Products,MXIM 64 | MercadoLibre,MELI 65 | Microchip Technology,MCHP 66 | "Micron Technology, Inc.",MU 67 | Microsoft Corporation,MSFT 68 | Moderna,MRNA 69 | Mondelēz International,MDLZ 70 | Monster Beverage Corporation,MNST 71 | "NetEase, Inc.",NTES 72 | Netflix,NFLX 73 | Nvidia Corporation,NVDA 74 | NXP Semiconductors N.V.,NXPI 75 | "O'Reilly Automotive, Inc.",ORLY 76 | PACCAR Inc.,PCAR 77 | "Paychex, Inc.",PAYX 78 | PayPal,PYPL 79 | "PepsiCo, Inc.",PEP 80 | Pinduoduo Inc.,PDD 81 | QUALCOMM,QCOM 82 | Regeneron Pharmaceuticals,REGN 83 | Ross Stores Inc.,ROST 84 | Seagen Inc.,SGEN 85 | "Sirius XM Radio, Inc.",SIRI 86 | "Skyworks Solutions, Inc.",SWKS 87 | Splunk,SPLK 88 | Starbucks Corporation,SBUX 89 | "Synopsys, Inc.",SNPS 90 | T-Mobile US,TMUS 91 | "Take-Two Interactive, Inc.",TTWO 92 | "Tesla, Inc.",TSLA 93 | Texas Instruments,TXN 94 | Trip.com Group,TCOM 95 | Ulta Beauty,ULTA 96 | Verisign,VRSN 97 | Verisk Analytics,VRSK 98 | Vertex Pharmaceuticals,VRTX 99 | "Walgreen Boots Alliance, Inc.",WBA 100 | "Workday, Inc.",WDAY 101 | "Xcel Energy, Inc.",XEL 102 | "Xilinx, Inc.",XLNX 103 | Zoom Video Communications,ZM 104 | -------------------------------------------------------------------------------- /stock_price_minute_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS stock_price_minute ( 2 | id INTEGER PRIMARY KEY, 3 | stock_id INTEGER, 4 | datetime NOT NULL, 5 | open NOT NULL, 6 | high NOT NULL, 7 | low NOT NULL, 8 | close NOT NULL, 9 | volume NOT NULL, 10 | FOREIGN KEY (stock_id) REFERENCES stock (id) 11 | ) 12 | --------------------------------------------------------------------------------