├── runtime.txt ├── Procfile ├── .gitignore ├── main.py ├── Pipfile ├── README.md ├── algo └── logic.py └── Pipfile.lock /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.5 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python main.py 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .envrc 3 | .vscode 4 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from algo import logic 2 | 3 | if __name__ == '__main__': 4 | logic.main() -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | alpaca-trade-api = "*" 8 | BeautifulSoup4 = "*" 9 | iexfinance = "*" 10 | 11 | [dev-packages] 12 | 13 | [requires] 14 | python_version = "3.6" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S&P100 replication algo for Alpaca API 2 | 3 | This is an algorithm that manage your portfolio by simply replication S&P100 index by 4 | buying the underlying stocks. 5 | 6 | ## Setup 7 | 8 | ``` 9 | $ pipenv install 10 | $ pipenv shell 11 | $ python main.py 12 | ``` 13 | 14 | This also works in Heroku. 15 | 16 | ## Customization 17 | 18 | ### Use of previous close prices 19 | The code currently uses the last close price from IEX to calculate the target portfolio, and place 20 | market orders at the market open, but you can change it so that it uses the current prices, and/or 21 | runs at the market close time to be more accurate. Though, it should not diverge much unless some 22 | big shares move 10+% overnight, which would also be corrected next day. 23 | 24 | ### Rebalance timing 25 | This algo rebalances everyday. You can change it to more frequently, or less. Less frequency should 26 | be fine using IEX daily data, but if you are going intraday rebalancing, you should use Alpaca/Polygon 27 | intraday data. 28 | 29 | ### Other indexes 30 | If you follow the code, you can easily change this for S&P500 or DIJA or whatever you like, as 31 | far as there is a list on the web. 32 | -------------------------------------------------------------------------------- /algo/logic.py: -------------------------------------------------------------------------------- 1 | import alpaca_trade_api as tradeapi 2 | 3 | from bs4 import BeautifulSoup 4 | import iexfinance as iex 5 | import logging 6 | import pandas as pd 7 | import requests 8 | import time 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | api = tradeapi.REST() 13 | 14 | 15 | def _fake_submit(*args, **kwargs): 16 | print(f'fake_submit({args}, {kwargs}))') 17 | 18 | # api.submit_order = _fake_submit 19 | 20 | 21 | def get_sp100(): 22 | '''Scrape wikipedia and returns the list of SP100 symbols''' 23 | 24 | url = 'https://en.wikipedia.org/wiki/S%26P_100' 25 | resp = requests.get(url) 26 | resp.raise_for_status() 27 | content = resp.content 28 | soup = BeautifulSoup(content, 'html.parser') 29 | 30 | return [ 31 | row.select('td')[0].text.strip() 32 | for row in soup.select('table')[2].select('tbody tr')[1:] 33 | ] 34 | 35 | 36 | def get_stockdata(symbols): 37 | '''Get stock data (key stats and previous) from IEX. 38 | Just deal with IEX's 99 stocks limit per request. 39 | ''' 40 | partlen = 99 41 | result = {} 42 | for i in range(0, len(symbols), partlen): 43 | part = symbols[i:i + partlen] 44 | kstats = iex.Stock(part).get_key_stats() 45 | previous = iex.Stock(part).get_previous() 46 | for symbol in part: 47 | kstats[symbol].update(previous[symbol]) 48 | result.update(kstats) 49 | 50 | return pd.DataFrame(result) 51 | 52 | 53 | def calc_target(api, stkdata): 54 | '''Returns a DataFrame with: 55 | - target_qty: calculated shares to be held 56 | - current_qty: current holding shares 57 | - last_close: last closing price 58 | - weight: portfolio weight based on the market cap 59 | - marketcap: market cap from IEX API 60 | 61 | Note current_qty may include symbols outside of sp100 list, 62 | which should be sold. These symbols will have 0 in marketcap 63 | in this DataFrame. 64 | ''' 65 | weights = (stkdata.T['marketcap'] / stkdata.T['marketcap'].sum()) 66 | pval = float(api.get_account().portfolio_value) 67 | 68 | target_qty = (pval * weights) // stkdata.T['close'] 69 | current_qty = pd.Series({ 70 | p.symbol: int(p.qty) 71 | for p in api.list_positions()}) 72 | 73 | return pd.DataFrame({ 74 | 'target_qty': target_qty, 75 | 'current_qty': current_qty, 76 | 'last_close': stkdata.T['close'], 77 | 'weight': weights, 78 | 'marketcap': stkdata.T['marketcap'], 79 | }).fillna(0) 80 | 81 | 82 | def submit_and_wait(orders, side): 83 | '''Submit orders and wait all of them go through.''' 84 | for symbol, qty in orders.items(): 85 | try: 86 | api.submit_order( 87 | symbol=symbol, 88 | side=side, 89 | qty=qty, 90 | type='market', 91 | time_in_force='day', 92 | ) 93 | except Exception as e: 94 | logger.error(e) 95 | while True: 96 | orders = api.list_orders() 97 | if len(orders) == 0: 98 | break 99 | time.sleep(1) 100 | 101 | 102 | def trade(df): 103 | '''Execute trade based on the target portfolio vs current''' 104 | diff = df['target_qty'] - df['current_qty'] 105 | 106 | # sell first, to have enough buying power back 107 | sells = {symbol: -int(qty) for symbol, qty in diff.items() if qty < 0} 108 | submit_and_wait(sells, 'sell') 109 | 110 | buys = {symbol: int(qty) for symbol, qty in diff.items() if qty > 0} 111 | submit_and_wait(buys, 'buy') 112 | 113 | 114 | def rebalance(): 115 | '''Get up-to-date symbol list and calculate optimal portfolio, then 116 | trade accordingly.''' 117 | symbols = get_sp100() 118 | stkdata = get_stockdata(symbols) 119 | df = calc_target(api, stkdata) 120 | trade(df) 121 | 122 | 123 | def main(): 124 | '''The main loop. Perform rebalance() in the morning of 125 | market open day. 126 | ''' 127 | open_dates = set([ 128 | c._raw['date'] for c in api.get_calendar()]) 129 | done = None 130 | while True: 131 | clock = api.get_clock() 132 | today = clock.timestamp.strftime('%Y-%m-%d') 133 | 134 | if today in open_dates and done != today: 135 | if clock.timestamp.time() >= pd.Timestamp('09:30').time(): 136 | rebalance() 137 | done = today 138 | time.sleep(30) 139 | 140 | 141 | if __name__ == '__main__': 142 | logging.basicConfig(level=logging.DEBUG) 143 | main() 144 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "89074854b318ce6b8a1174f02a6cee30586e9b3a56b75bd3c1481d6f826c9bd7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alpaca-trade-api": { 20 | "hashes": [ 21 | "sha256:febf996529befa6e99b7ce27fa42c27ccbd292274871c628b7ca825d8ef23047" 22 | ], 23 | "index": "pypi", 24 | "version": "==0.15" 25 | }, 26 | "asyncio-nats-client": { 27 | "hashes": [ 28 | "sha256:213943d0d81654fa333722810203565af160a795e8e7e7319fca9339a0d5a1d8" 29 | ], 30 | "version": "==0.7.2" 31 | }, 32 | "beautifulsoup4": { 33 | "hashes": [ 34 | "sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", 35 | "sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", 36 | "sha256:808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89" 37 | ], 38 | "index": "pypi", 39 | "version": "==4.6.0" 40 | }, 41 | "certifi": { 42 | "hashes": [ 43 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 44 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 45 | ], 46 | "version": "==2018.4.16" 47 | }, 48 | "chardet": { 49 | "hashes": [ 50 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 51 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 52 | ], 53 | "version": "==3.0.4" 54 | }, 55 | "idna": { 56 | "hashes": [ 57 | "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", 58 | "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 59 | ], 60 | "version": "==2.7" 61 | }, 62 | "iexfinance": { 63 | "hashes": [ 64 | "sha256:e68e79f1cdcd6170eb36eecd8b9d37f8a4a33c48953a66c2548c423b0171618c", 65 | "sha256:f15e9568b81e32b0e777fa75a7d57875022554a0c69b91a7b703619fb3153354" 66 | ], 67 | "index": "pypi", 68 | "version": "==0.3.4" 69 | }, 70 | "numpy": { 71 | "hashes": [ 72 | "sha256:07379fe0b450f6fd6e5934a9bc015025bb4ce1c8fbed3ca8bef29328b1bc9570", 73 | "sha256:085afac75bbc97a096744fcfc97a4b321c5a87220286811e85089ae04885acdd", 74 | "sha256:2d6481c6bdab1c75affc0fc71eb1bd4b3ecef620d06f2f60c3f00521d54be04f", 75 | "sha256:2df854df882d322d5c23087a4959e145b953dfff2abe1774fec4f639ac2f3160", 76 | "sha256:381ad13c30cd1d0b2f3da8a0c1a4aa697487e8bb0e9e0cbeb7439776bcb645f8", 77 | "sha256:385f1ce46e08676505b692bfde918c1e0b350963a15ef52d77691c2cf0f5dbf6", 78 | "sha256:4130e5ae16c656b7de654dc5e595cfeb85d3a4b0bb0734d19c0dce6dc7ee0e07", 79 | "sha256:4d278c2261be6423c5e63d8f0ceb1b0c6db3ff83f2906f4b860db6ae99ca1bb5", 80 | "sha256:51c5dcb51cf88b34b7d04c15f600b07c6ccbb73a089a38af2ab83c02862318da", 81 | "sha256:589336ba5199c8061239cf446ee2f2f1fcc0c68e8531ee1382b6fc0c66b2d388", 82 | "sha256:5ae3564cb630e155a650f4f9c054589848e97836bebae5637240a0d8099f817b", 83 | "sha256:5edf1acc827ed139086af95ce4449b7b664f57a8c29eb755411a634be280d9f2", 84 | "sha256:6b82b81c6b3b70ed40bc6d0b71222ebfcd6b6c04a6e7945a936e514b9113d5a3", 85 | "sha256:6c57f973218b776195d0356e556ec932698f3a563e2f640cfca7020086383f50", 86 | "sha256:758d1091a501fd2d75034e55e7e98bfd1370dc089160845c242db1c760d944d9", 87 | "sha256:8622db292b766719810e0cb0f62ef6141e15fe32b04e4eb2959888319e59336b", 88 | "sha256:8b8dcfcd630f1981f0f1e3846fae883376762a0c1b472baa35b145b911683b7b", 89 | "sha256:91fdd510743ae4df862dbd51a4354519dd9fb8941347526cd9c2194b792b3da9", 90 | "sha256:97fa8f1dceffab782069b291e38c4c2227f255cdac5f1e3346666931df87373e", 91 | "sha256:9b705f18b26fb551366ab6347ba9941b62272bf71c6bbcadcd8af94d10535241", 92 | "sha256:9d69967673ab7b028c2df09cae05ba56bf4e39e3cb04ebe452b6035c3b49848e", 93 | "sha256:9e1f53afae865cc32459ad211493cf9e2a3651a7295b7a38654ef3d123808996", 94 | "sha256:a4a433b3a264dbc9aa9c7c241e87c0358a503ea6394f8737df1683c7c9a102ac", 95 | "sha256:baadc5f770917ada556afb7651a68176559f4dca5f4b2d0947cd15b9fb84fb51", 96 | "sha256:c725d11990a9243e6ceffe0ab25a07c46c1cc2c5dc55e305717b5afe856c9608", 97 | "sha256:d696a8c87315a83983fc59dd27efe034292b9e8ad667aeae51a68b4be14690d9", 98 | "sha256:e1864a4e9f93ddb2dc6b62ccc2ec1f8250ff4ac0d3d7a15c8985dd4e1fbd6418", 99 | "sha256:e1d18421a7e2ad4a655b76e65d549d4159f8874c18a417464c1d439ee7ccc7cd" 100 | ], 101 | "version": "==1.14.5" 102 | }, 103 | "pandas": { 104 | "hashes": [ 105 | "sha256:05ac350f8a35abe6a02054f8cf54e0c048f13423b2acb87d018845afd736f0b4", 106 | "sha256:174543cd68eaee60620146b38faaed950071f5665e0a4fa4adfdcfc23d7f7936", 107 | "sha256:1a62a237fb7223c11d09daaeaf7d15f234bb836bfaf3d4f85746cdf9b2582f99", 108 | "sha256:2c1ed1de5308918a7c6833df6db75a19c416c122921824e306c64a0626b3606c", 109 | "sha256:33825ad26ce411d6526f903b3d02c0edf627223af59cf4b5876aa925578eec74", 110 | "sha256:4c5f76fce8a4851f65374ea1d95ca24e9439540550e41e556c0879379517a6f5", 111 | "sha256:67504a96f72fb4d7f051cfe77b9a7bb0d094c4e2e5a6efb2769eb80f36e6b309", 112 | "sha256:683e0cc8c7faececbbc06aa4735709a07abad106099f165730c1015da916adec", 113 | "sha256:77cd1b485c6a860b950ab3a85be7b5683eaacbc51cadf096db967886607d2231", 114 | "sha256:814f8785f1ab412a7e9b9a8abb81dfe8727ebdeef850ecfaa262c04b1664000f", 115 | "sha256:894216edaf7dd0a92623cdad423bbec2a23fc06eb9c85483e21876d1ef8f47e9", 116 | "sha256:9331e20a07360b81d8c7b4b50223da387d264151d533a5a5853325800e6631a4", 117 | "sha256:9cd3614b4e31a0889388ff1bd19ae857ad52658b33f776065793c293a29cf612", 118 | "sha256:9d79e958adcd037eba3debbb66222804171197c0f5cd462315d1356aa72a5a30", 119 | "sha256:b90e5d5460f23607310cbd1688a7517c96ce7b284095a48340d249dfc429172e", 120 | "sha256:bc80c13ffddc7e269b706ed58002cc4c98cc135c36d827c99fb5ca54ced0eb7a", 121 | "sha256:cbb074efb2a5e4956b261a670bfc2126b0ccfbf5b96b6ed021bc8c8cb56cf4a8", 122 | "sha256:e8c62ab16feeda84d4732c42b7b67d7a89ad89df7e99efed80ea017bdc472f26", 123 | "sha256:ff5ef271805fe877fe0d1337b6b1861113c44c75b9badb595c713a72cd337371" 124 | ], 125 | "version": "==0.23.3" 126 | }, 127 | "python-dateutil": { 128 | "hashes": [ 129 | "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", 130 | "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" 131 | ], 132 | "version": "==2.7.3" 133 | }, 134 | "pytz": { 135 | "hashes": [ 136 | "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", 137 | "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" 138 | ], 139 | "version": "==2018.5" 140 | }, 141 | "requests": { 142 | "hashes": [ 143 | "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", 144 | "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" 145 | ], 146 | "version": "==2.19.1" 147 | }, 148 | "six": { 149 | "hashes": [ 150 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 151 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 152 | ], 153 | "version": "==1.11.0" 154 | }, 155 | "urllib3": { 156 | "hashes": [ 157 | "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", 158 | "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" 159 | ], 160 | "version": "==1.23" 161 | }, 162 | "websocket-client": { 163 | "hashes": [ 164 | "sha256:18f1170e6a1b5463986739d9fd45c4308b0d025c1b2f9b88788d8f69e8a5eb4a", 165 | "sha256:db70953ae4a064698b27ae56dcad84d0ee68b7b43cb40940f537738f38f510c1" 166 | ], 167 | "version": "==0.48.0" 168 | }, 169 | "websockets": { 170 | "hashes": [ 171 | "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", 172 | "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", 173 | "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", 174 | "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", 175 | "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", 176 | "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", 177 | "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", 178 | "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", 179 | "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", 180 | "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", 181 | "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", 182 | "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", 183 | "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", 184 | "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", 185 | "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", 186 | "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", 187 | "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", 188 | "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", 189 | "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", 190 | "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", 191 | "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" 192 | ], 193 | "version": "==6.0" 194 | } 195 | }, 196 | "develop": {} 197 | } 198 | --------------------------------------------------------------------------------