Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Hitoshi Harada committed Jun 26, 2018
0 parents commit eb08112
Show file tree
Hide file tree
Showing 13 changed files with 1,480 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
.envrc
.vscode
15 changes: 15 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
alpaca-trade-api = "==0.12"


[dev-packages]
jupyter = "*"
matplotlib = "*"

[requires]
python_version = "3.6"
592 changes: 592 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
worker: python main.py
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# A Buy-on-dip algo for Alpaca API

This is a simple algo that trades every day refreshing portfolio based on the EMA ranking.
Among the universe (e.g. SP500 stocks), it ranks by daily (price - EMA) percentage as of
trading time and keep positions in sync with lowest ranked stocks.

The rationale behind this: low (price - EMA) vs price ratio indicates there is a big dip
in a short time. Since the universe is SP500 which means there is some fundamental strengths,
the belief is that the price should be recovered to some extent.

## How to run

The only dependency is alpaca-trade-api module. You can set up the environment by
pipenv. If python 3 and the dependency is ready,

```
$ python main.py
```

That's it.

Also, this repository is set up for Heroku. If you have Heroku account, create a new
app and run this as an application. It is only one worker app so make sure you set up
worker type app.


## Cutomization

universe.Universe is hard-coded. Easy customization is to change this to more dynamic
set of stocks with some filters such as per-share price to be less than $50 or so.
Some of the numbers are also hard-coded and it is meant to run in an account with about
$500 deposit, with asuumption that one position to be up to $100, resulting in 5 positions
at most. If your account size and position size preference are different, you can
change these valuess.

EMA-5 is also very arbitrary choice. You could try something like 10, too.

## Future work

There is btest.py that runs a simple simulation. This module needs more easy visualization
and more integrated setup, possibly using jupyter and matplotlib.
4 changes: 4 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from samplealgo import algo

if __name__ == '__main__':
algo.main()
5 changes: 5 additions & 0 deletions playground.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import subprocess

pypath = os.path.abspath(os.path.dirname(__file__))
subprocess.run(f"PYTHONPATH={pypath} jupyter notebook --notebook-dir=notes", shell=True, check=True)
1 change: 1 addition & 0 deletions runtime.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-3.6.5
Empty file added samplealgo/__init__.py
Empty file.
188 changes: 188 additions & 0 deletions samplealgo/algo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import alpaca_trade_api as tradeapi
import pandas as pd
import time
import logging

from .universe import Universe

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

NY = 'America/New_York'
api = tradeapi.REST()


def _dry_run_submit(*args, **kwargs):
logging.info(f'submit({args}, {kwargs})')
# api.submit_order =_dry_run_submit


def prices(symbols):
'''Get the map of prices in DataFrame with the symbol name key.'''
now = pd.Timestamp.now(tz=NY)
end_dt = now
if now.time() >= pd.Timestamp('09:30', tz=NY).time():
end_dt = now - \
pd.Timedelta(now.strftime('%H:%M:%S')) - pd.Timedelta('1 minute')
result = api.list_bars(
symbols,
'1D',
end_dt=end_dt.isoformat(),
limit=1200)
return {
ab.symbol: ab.df for ab in result
}


def calc_scores(dfs, dayindex=-1):
'''Calculate scores based on the indicator and
return the sorted result.
'''
diffs = {}
param = 10
for symbol, df in dfs.items():
if len(df.close.values) <= param:
continue
ema = df.close.ewm(span=param).mean()[dayindex]
last = df.close.values[dayindex]
diff = (last - ema) / last
diffs[symbol] = diff

return sorted(diffs.items(), key=lambda x: x[1])


def get_orders(api, dfs, position_size=100, max_positions=5):
'''Calculate the scores with the universe to build the optimal
portfolio as of today, and extract orders to transition from
current portfolio to the calculated state.
'''
# rank the stocks based on the indicators.
ranked = calc_scores(dfs)
to_buy = set()
to_sell = set()
account = api.get_account()
# take the top one twentieth out of ranking,
# excluding stocks too expensive to buy a share
for symbol, _ in ranked[:len(ranked) // 20]:
price = float(dfs[symbol].close.values[-1])
if price > float(account.cash):
continue
to_buy.add(symbol)

# now get the current positions and see what to buy,
# what to sell to transition to today's desired portfolio.
positions = api.list_positions()
logger.info(positions)
holdings = {p.symbol: p for p in positions}
holding_symbol = set(holdings.keys())
to_sell = holding_symbol - to_buy
to_buy = to_buy - holding_symbol
orders = []

# if a stock is in the portfolio, and not in the desired
# portfolio, sell it
for symbol in to_sell:
shares = holdings[symbol].qty
orders.append({
'symbol': symbol,
'qty': shares,
'side': 'sell',
})
logger.info(f'order(sell): {symbol} for {shares}')

# likewise, if the portfoio is missing stocks from the
# desired portfolio, buy them. We sent a limit for the total
# position size so that we don't end up holding too many positions.
max_to_buy = max_positions - (len(positions) - len(to_sell))
for symbol in to_buy:
if max_to_buy <= 0:
break
shares = position_size // float(dfs[symbol].close.values[-1])
if shares == 0.0:
continue
orders.append({
'symbol': symbol,
'qty': shares,
'side': 'buy',
})
logger.info(f'order(buy): {symbol} for {shares}')
max_to_buy -= 1
return orders


def trade(orders, wait=30):
'''This is where we actually submit the orders and wait for them to fill.
This is an important step since the orders aren't filled atomically,
which means if your buys come first with littme cash left in the account,
the buy orders will be bounced. In order to make the transition smooth,
we sell first and wait for all the sell orders to fill and then submit
buy orders.
'''

# process the sell orders first
sells = [o for o in orders if o['side'] == 'sell']
for order in sells:
try:
logger.info(f'submit(sell): {order}')
api.submit_order(
symbol=order['symbol'],
qty=order['qty'],
side='sell',
type='market',
time_in_force='day',
)
except Exception as e:
logger.error(e)
count = wait
while count > 0:
pending = api.list_orders()
if len(pending) == 0:
logger.info(f'all sell orders done')
break
logger.info(f'{len(pending)} sell orders pending...')
time.sleep(1)
count -= 1

# process the buy orders next
buys = [o for o in orders if o['side'] == 'buy']
for order in buys:
try:
logger.info(f'submit(buy): {order}')
api.submit_order(
symbol=order['symbol'],
qty=order['qty'],
side='buy',
type='market',
time_in_force='day',
)
except Exception as e:
logger.error(e)
count = wait
while count > 0:
pending = api.list_orders()
if len(pending) == 0:
logger.info(f'all buy orders done')
break
logger.info(f'{len(pending)} buy orders pending...')
time.sleep(1)
count -= 1


def main():
'''The entry point. Goes into an infinite loop and
start trading every morning at the market open.'''
done = None
logging.info('start running')
while True:
now = pd.Timestamp.now(tz=NY)
if 0 <= now.dayofweek <= 4 and done != now.strftime('%Y-%m-%d'):
if now.time() >= pd.Timestamp('09:30', tz=NY).time():
dfs = prices(Universe)
orders = get_orders(api, dfs)
trade(orders)
# flag it as done so it doesn't work again for the day
# TODO: this isn't tolerant to the process restart
done = now.strftime('%Y-%m-%d')
logger.info(f'done for {done}')

time.sleep(1)
Loading

0 comments on commit eb08112

Please sign in to comment.