forked from alpacahq/samplealgo01
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Hitoshi Harada
committed
Jun 26, 2018
0 parents
commit eb08112
Showing
13 changed files
with
1,480 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__pycache__ | ||
.envrc | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
worker: python main.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from samplealgo import algo | ||
|
||
if __name__ == '__main__': | ||
algo.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
python-3.6.5 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.