#!/usr/bin/env python3
import sys
import os
import re
import json
import requests
import numpy as np
import argparse
from colorama import init, Fore, Back, Style
from influxdb import InfluxDBClient
import time
import datetime
import webbrowser
from multiprocessing.dummy import Pool
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
import plotly.graph_objects as plot
import plotly.express as px
import pandas as pd
init()
version = "2.0.6"
# set directory to user's home (should work for Linux, Mac and Windows)
os.chdir(os.path.expanduser("~"))
# command line options and help
parser = argparse.ArgumentParser()
parser.add_argument("-a","--add", nargs='+', help="add a symbol, the quantity held and the price paid")
parser.add_argument("-c","--costs", help="displays cost, quantity, price paid and target prices",action="store_true")
parser.add_argument("-ca","--cash", nargs='+', help="add an amount of cash held in the portfolio")
parser.add_argument("-ch","--chart_holdings", help="display a pie chart of current holdings",action="store_true")
parser.add_argument("-cv","--chart_value", help="display the value of the portfolio over time",action="store_true")
parser.add_argument("-d","--delete", nargs='+', help="delete a symbol")
parser.add_argument("-f","--fundamentals", help="get target price, upside, P/E and 52 week Highs & Lows",action="store_true")
parser.add_argument("-g","--dailygain", help="display stocks by today's gainers and losers",action="store_true")
parser.add_argument("-G","--totalgain", help="display stocks by total gainers and losers",action="store_true")
parser.add_argument("-i","--influx", nargs='+', help="influx server, port, user and password")
parser.add_argument("-k","--key", nargs='+', help="add an api key, see https://www.alphavantage.co/")
parser.add_argument("-n","--news", nargs='+', help="opens news page(s) for symbols in your browser")
parser.add_argument("-o","--offline", help="displays last downloaded data",action="store_true")
parser.add_argument("-p","--portfolio", help="choose a portfolio")
parser.add_argument("-q","--quote", nargs='+', help="gets quote for single stock")
parser.add_argument("-r","--repeat", nargs='+', help="pull data every N minutes")
parser.add_argument("-s","--stats", nargs='+', help="gets stats for single stock")
parser.add_argument("-t","--target", nargs='+', help="set a high and low target price for a symbol")
parser.add_argument("-v","--version", help="print the version and exit",action="store_true")
args = parser.parse_args()
if args.add:
if len(args.add)!=3:
print('symbol, quantity and price are required with --add')
sys.exit(1)
if args.delete:
if len(args.delete)!=1:
print('a symbol is required with --delete')
sys.exit(1)
if args.influx:
if len(args.influx)!=4:
print('The influxdb server, port #, user and password are required with --influx')
sys.exit(1)
if args.news:
if len(args.news)<1:
print('a symbol is required with --news')
sys.exit(1)
else:
for symbol in (args.news):
webbrowser.open('https://seekingalpha.com/symbol/'+symbol, new=2)
sys.exit(1)
if args.repeat:
if len(args.repeat)!=1:
print('Number of Minutes is required with --repeat')
sys.exit(1)
if args.portfolio:
portfolio=str("." + (args.portfolio))
if os.path.exists(portfolio)==False:
os.mkdir(portfolio)
os.chdir(os.path.expanduser(portfolio))
if args.target:
if len(args.target)!=3:
print('A symbol with high and low targets are required with --target')
sys.exit(1)
if args.version:
print('version ' + version)
sys.exit(1)
if args.key:
apikey=str(args.key[0])
print("Testing key " + apikey)
url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=IBM&apikey="+apikey
response = (requests.get(url))
print(response)
if (response.status_code) == 200:
print("passed")
np.save('key.npy', apikey)
sys.exit(1)
else:
print('failed, please get a valid key from https://www.alphavantage.co')
sys.exit(1)
if args.cash:
if len(args.cash)!=1:
print('a dollar amount is required with --cash')
sys.exit(1)
if os.path.exists('key.npy')==False:
print("Please add a valid key")
sys.exit(1)
else:
key = np.load('key.npy', allow_pickle=True).item()
if os.path.exists('stocks.npy')==False:
stocks={}
else:
stocks = np.load('stocks.npy', allow_pickle=True).item()
if os.path.exists('cost.npy')==False:
cost={}
else:
cost = np.load('cost.npy', allow_pickle=True).item()
if os.path.exists('last.npy')==False:
last={}
else:
last = np.load('last.npy', allow_pickle=True).item()
if os.path.exists('target.npy')==False:
target={}
else:
target = np.load('target.npy', allow_pickle=True).item()
Gain={}
value = cost.copy()
day = cost.copy()
# function to call url
def callurl(url):
try:
r = requests.get(url)
except requests.exceptions.RequestException as e:
print('http timeout getting ' + symbol)
sys.exit(1)
if r.status_code == 404:
print(symbol + ' is unknown')
sys.exit(1)
r = json.dumps(r.json(), indent=4)
print(highlight(r, JsonLexer(), TerminalFormatter()))
sys.exit(1)
# function to add stock, quantity and price. checks if stock is valid
def inputtostocks(symbol, qtn, price):
url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol="+symbol+"&apikey="+key
try:
r = requests.get(url)
except requests.exceptions.RequestException as e:
print('http timeout getting ' + symbol)
sys.exit(1)
r = json.dumps(r.json())
r = json.loads(r)
if (len(r['Global Quote'])) == 0:
print(symbol + ' is unknown')
sys.exit(1)
paid=float(qtn*price)
stocks.update({symbol:qtn})
np.save('stocks.npy', stocks)
cost.update({symbol:paid})
np.save('cost.npy', cost)
last.update({symbol:[price,0,0,0,0]})
np.save('last.npy', last)
getprice(symbol)
printstock(symbol)
# function to remove a stock
def removestocks(sym):
if sym in stocks:
del stocks[sym]
np.save('stocks.npy', stocks)
if sym in cost:
del cost[sym]
del value[sym]
del day[sym]
np.save('cost.npy', cost)
if sym in last:
del last[sym]
np.save('last.npy', last)
if sym in target:
del target[sym]
np.save('target', target)
return
# get current price
def getprice(symbol):
try:
url = "https://www.marketbeat.com/stocks/NASDAQ/"+symbol
r = requests.get(url)
if (r.status_code) != 200:
time.sleep(1)
r = requests.get(url)
r = r.text
for line in r.split('\n'):
if re.search('
', line):
found=str(line)
price = float((((found.split('
', 1)[1]).split('>')[1]).split('<')[0]).replace('$', '').replace(',',''))
change = float(found.split('
', 1)[1].split('&')[0].split('"> ')[1])
percent = found.split('
', 1)[1].split(' ')[1].split('%')[0].split('(')[1]
prev = (price - change)
change = str(change)
gain = str(round(((float(price)/float(prev))-1)*100,2))
totalgain = (str(round(((stocks[symbol])*(float(last[symbol][0]))) - (cost[symbol]))))
last.update({symbol:[price,change,percent,gain,totalgain]})
except requests.exceptions.RequestException as e:
print('http timeout getting ' + symbol)
return
def fundamentals():
print('{:<8}'.format ('Symbol') + '{:<9}'.format ('Price') + '{:<9}'.format ('Target') + '{:<9}'.format ('Upside') + '{:<8}'.format ('P/E') + '{:<8}'.format ('Yield') + '{:<9}'.format ('High52') + '{:<9}'.format ('Low52') + '{:<9}'.format ('Earnings'))
for symbol in sorted(stocks.keys()):
url = "https://www.marketbeat.com/stocks/NASDAQ/"+symbol
r = requests.get(url).text
for line in r.split('\n'):
if re.search('consensus price target of', line):
found=str(line)
th=float(((found.split('consensus price target of ', 1)[1]).split(' ')[0]).replace(',', '').replace('$', ''))
tp=(target[symbol][0])
if symbol in target:
tl=float(target[symbol][1])
else:
tl=0
t=th,tl
target.update({symbol:t})
np.save('target.npy', target)
break
else:
th='N/A'
for line in r.split('\n'):
if re.search('
', line):
found=str(line)
price = float(found.split('
', 1)[1].split('>')[1].split('<')[0].replace('$', '').replace(',',''))
break
if (type(th) == float):
upside=round(th/price, 4)
else:
upside='N/A'
for line in r.split('\n'):
if re.search('
', line):
found=str(line)
if re.search('P/E Ratio- ', found):
pe = found.split('P/E Ratio
- ')[1].split('<')[0]
else:
pe = 'N/A'
high52 = found.split('52-Week Range')[1].split('
$')[1].split('<')[0].replace(',','')
low52 = found.split('52-Week Range')[1].split('>$')[1].split('<')[0].replace(',','')
Yield = found.split('Yield
')[1].split('<')[0]
break
for line in r.split('\n'):
if re.search('Next Earnings', line):
found=str(line)
next_earnings = found.split('Next Earnings')[1].split('')[1].split('')[0]
break
else:
next_earnings = 'N/A'
col=(Style.RESET_ALL)
if 'tp' in locals():
if isinstance(tp, float) and isinstance(th, float):
if th > tp:
col=(Fore.GREEN)
elif th < tp:
col=(Fore.RED)
else:
col=(Style.RESET_ALL)
print('{:<8}'.format (symbol) + '{:<9}'.format (price) + col + '{:<9}'.format (th) + Style.RESET_ALL + '{:<9}'.format (upside) + '{:<8}'.format (pe) + '{:<8}'.format (Yield) + '{:<9}'.format (high52) + '{:<9}'.format (low52) + '{:<9}'.format (next_earnings), sep='\n')
def printstock(symbol):
if (str((last[symbol][1])[0]))=='-':
col=(Fore.RED)
else:
col=(Fore.GREEN)
if (((stocks[symbol])*(float(last[symbol][0]))) -(cost[symbol])) <0:
tcol=(Fore.RED)
else:
tcol=(Fore.GREEN)
day[symbol]=(float(last[symbol][1])*(stocks[symbol]))
value[symbol] =((stocks[symbol])*(float(last[symbol][0])))
print('{:<5}'.format (symbol) + ' $' + '{:<7}'.format (last[symbol][0]),
col + ' ' + '{:<8}'.format (last[symbol][1]) + '{:<7}'.format (str(last[symbol][2]) + '%') + Style.RESET_ALL, 'value $' + '{:<10}'.format (str(round((stocks[symbol])*(float(last[symbol][0])),2))),
'gain $' + tcol + '{:<11}'.format (str(round(((stocks[symbol])*(float(last[symbol][0]))) - (cost[symbol]),2))) + str(round(100*((((stocks[symbol])*(float(last[symbol][0]))) - (cost[symbol]))/(cost[symbol])),2))+'%'+ Style.RESET_ALL)
return
def printcosts(symbol):
paid=(str(round(float((cost[symbol])/(stocks[symbol])),4)))
if symbol in target:
Target =str(target[symbol])
else:
Target = 'None'
print('{:<6}'.format (symbol) + 'Cost $' + '{:<12}'.format (str(round(float(cost[symbol]),2))) + ' ' + 'Qty ' + '{:<6}'.format (str(round(float(stocks[symbol])))) + ' ' + 'Price $' +'{:<8}'.format (str(paid)) + ' ' 'Targets ' + Target)
return
def printtotal():
gain=sum(day.values())
if (gain)<0:
pgain=(Fore.RED + str(round(gain)))
else:
pgain=(Fore.GREEN + str(round(gain)))
mkt=sum(value.values())
cst=sum(cost.values())
tgain=(round((mkt-cst)))
pmkt=str(round(mkt))
if (tgain)<0:
ptgain=(Fore.RED + str(tgain))
else:
ptgain=(Fore.GREEN + str(tgain))
if os.path.exists('cash.npy')==True:
cash=np.load('cash.npy', allow_pickle=True).item()
print(('Cash $') + str(cash))
pmkt=str(round(mkt+cash))
print()
print('Total $' + '{:<9}'.format (pmkt) + '{:<13}'.format (ptgain) + '{:<7}'.format (str(round((100*(mkt-cst)/cst),2)) +'%') + Style.RESET_ALL, 'Daily $' + '{:<14}'.format (pgain) + str(round((100*(gain/mkt)),2))+'%' + Style.RESET_ALL)
date = (datetime.date.today())
if os.path.exists('value.npy')==False:
v = {}
v.update({str(date):float(pmkt)})
np.save('value.npy', v)
else:
v = np.load('value.npy', allow_pickle=True).item()
v.update({str(date):float(pmkt)})
np.save('value.npy', v)
def checktargets():
print()
for key in sorted(stocks.keys()):
if key in target :
if (float(last[key][0]) > float(target[key][0])):
print(key,'is above your target of', float(target[key][0]), 'at', float(last[key][0]))
if (float(last[key][0]) < float(target[key][1])):
print(key,'is below your target of', float(target[key][1]), 'at', float(last[key][0]))
if args.add:
symbol=str(args.add[0])
qtn=int(args.add[1])
price=float(args.add[2])
inputtostocks(symbol,qtn,price)
sys.exit(1)
if args.cash:
cash=int(args.cash[0])
np.save('cash.npy', cash)
sys.exit(1)
if args.target:
symbol=str(args.target[0])
th=float(args.target[1])
tl=float(args.target[2])
if tl > th:
th=(args.target[2])
tl=(args.target[1])
t=th,tl
if symbol in stocks:
target.update({symbol:t})
np.save('target.npy', target)
else:
print('Please input stock first with --add')
sys.exit(1)
sys.exit(1)
if args.chart_holdings:
holdings = {}
for symbol in sorted(stocks.keys()):
value = (float(last[symbol][0]) * float(stocks[symbol]))
holdings.update({symbol:value})
df = pd.DataFrame(holdings.items(), columns = ['stock', 'value'])
total = str(round(sum(holdings.values()),2))
fig = px.pie(df, title='Current holdings $' + total, values='value', names='stock', hole=0.1)
fig.update_traces(textposition='inside', textinfo='percent+label')
fig.show()
sys.exit(1)
if args.chart_value:
if os.path.exists('value.npy')==False:
print('No values recorded yet')
sys.exit(1)
else:
fig = plot.Figure()
totalvalue=np.load('value.npy', allow_pickle=True).item()
df = pd.DataFrame(totalvalue.items(), columns = ['date', 'value'])
fig.add_trace(plot.Scatter(x=list(df.date), y=list(df.value), showlegend=True, name='value', connectgaps=True))
fig.update_layout(template='simple_white', title_text="Portfolio value over time")
fig.update_yaxes(title='$ value')
fig.update_layout(xaxis=dict(rangeselector=dict(buttons=list([dict(count=1, label="1m",step="month",stepmode="backward"),dict(count=6,label="6m", step="month",stepmode="backward"),dict(count=1,label="YTD",step="year", stepmode="todate"),dict(count=1,label="1y",step="year", stepmode="backward"),dict(step="all")])),rangeslider=dict(visible=True),type="date"))
fig.show()
sys.exit(1)
if args.quote:
symbol=str(args.quote[0])
url = "https://www.marketbeat.com/stocks/NASDAQ/"+symbol
r = requests.get(url)
if (r.status_code) != 200:
time.sleep(1)
r = requests.get(url)
r = r.text
for line in r.split('\n'):
if re.search('', line):
found=str(line)
price = found.split('
', 1)[1].split('>')[1].split('<')[0].replace('$', '')
change = found.split('
', 1)[1].split('&')[0].split('"> ')[1]
percent = found.split('
', 1)[1].split(' ')[1].split('%')[0].split('(')[1]
for line in r.split('\n'):
if re.search('
', line):
found=str(line)
pe = found.split('P/E Ratio- ')[1].split('<')[0]
high52 = found.split('52-Week Range')[1].split('
$')[1].split('<')[0]
low52 = found.split('52-Week Range')[1].split('>$')[1].split('<')[0]
if (change[0])=='-':
change=(Fore.RED + str(change))
else:
change=(Fore.GREEN + str(change))
print ()
print (symbol)
print ('Price : ' + (str(price)))
print ('Change : ' + (change) + Style.RESET_ALL)
print ('PE ratio : ' + (pe))
print ('52 week high : ' + (high52))
print ('52 week low : ' + (low52))
if not args.stats:
sys.exit(1)
if args.stats:
symbol=str(args.stats[0])
url = "https://www.alphavantage.co/query?function=OVERVIEW&symbol="+symbol+"&apikey="+key
callurl(url)
# call removestocks if --delete option on command line
if args.delete:
sym=str(args.delete[0])
removestocks(sym)
sys.exit(1)
# polite exit if no stocks in dictionary and --added not used
if len(stocks)==0:
print('Please input stocks with --add')
sys.exit(1)
# operation modes
def influx():
server=str(args.influx[0])
port=int(args.influx[1])
user=str(args.influx[2])
password=str(args.influx[3])
metric = "Stocks"
database = "stock_quote"
series = []
if args.portfolio:
database=str(args.portfolio)
# Call getprice to update data
pool = Pool(len(stocks))
pool.map(getprice, stocks.keys())
for symbol in sorted(stocks.keys()):
s=(float(last[symbol][0])) #current price
change=(float(last[symbol][1]))
value=(round(float(last[symbol][0])*(stocks[symbol]),2))
gain=(round(value-(cost[symbol]),2))
pointValues = {
"time": datetime.datetime.today(),
"measurement": metric,
'fields': {
'price': s,
'change' : change,
'value': value,
'gain' : gain,
},
'tags': {
"Stock": symbol,
},
}
series.append(pointValues)
client = InfluxDBClient(server, port, user, password, database)
client.create_database(database)
client.write_points(series)
def offline():
os.system('clear')
print("Offline Mode")
for key in sorted(stocks.keys()):
printstock(key)
printtotal()
checktargets()
def costs():
os.system('clear')
for key in sorted(stocks.keys()):
printcosts(key)
print()
def online():
print("fetching data for " + (str(len(stocks))) + " stocks ...")
pool = Pool(len(stocks))
pool.map(getprice, stocks.keys())
os.system('clear')
for key in sorted(stocks.keys()):
printstock(key)
np.save('last.npy', last)
printtotal()
checktargets()
def totalgain():
print("fetching data for " + (str(len(stocks))) + " stocks ...")
pool = Pool(len(stocks))
pool.map(getprice, stocks.keys())
os.system('clear')
for stock in sorted(last.items(), key=lambda x:float(x[1][4]), reverse=True):
printstock(stock[0])
printtotal()
checktargets()
def dailygain():
print("fetching data for " + (str(len(stocks))) + " stocks ...")
pool = Pool(len(stocks))
pool.map(getprice, stocks.keys())
os.system('clear')
for stock in sorted(last.items(), key=lambda x:float(x[1][2]), reverse=True):
printstock(stock[0])
np.save('last.npy', last)
printtotal()
checktargets()
if args.repeat:
interval=(60*int(args.repeat[0]))
while True:
if args.influx:
influx()
elif args.offline:
offline()
else:
online()
time.sleep(interval)
elif args.influx:
influx()
elif args.offline:
offline()
elif args.costs:
costs()
elif args.fundamentals:
fundamentals()
elif args.dailygain:
dailygain()
elif args.totalgain:
totalgain()
else:
online()