In [1]:
#!/usr/bin/env python

%matplotlib inline

from bs4 import BeautifulSoup
from glob import glob
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import cookielib
import json
import math
import matplotlib.pyplot as plt
import mechanize
import numpy as np
import os
import re
import requests
import string
import sys
import time
import unidecode
import unicodedata
import urllib
import urllib2
import pandas as pd
from pandas.io.data import get_quote_yahoo
import locale
locale.setlocale( locale.LC_ALL, '' )


Out[1]:
'en_AU.UTF-8'

In [2]:
# codes = {}
# codes["AAPL"] = "individual"
# codes["EEM"] =  "Emerging"
# codes["EFA"] =  "Foreign"
# codes["GOOG"] = "individual"
# codes["GOOGL"] = "individual"
# codes["IVV"] = "Domestic"
# codes["RWR"] =  "RealEstate"
# codes["SHY"] = "BndsShrt"
# codes["TIP"] =  "USTIPS"
# codes["TLT"] = "BndsLng"

# with open("assetclass.json", 'w') as file_handle:
#     json.dump(codes, file_handle)

In [3]:
# examples = [{"username": "".join(np.random.choice([letter for letter in string.digits], 8)),
#              "passwd": "".join(np.random.choice([letter for letter in string.ascii_letters], 16)),
#              "url": "https://trading.scottrade.com/default.aspx"},
#             {"username": "".join(np.random.choice([letter for letter in string.digits], 8)),
#              "passwd": "".join(np.random.choice([letter for letter in string.ascii_letters], 16)),
#              "url": "https://trading.scottrade.com/default.aspx"}]
             
# with open("config-example.json", 'w') as file_handle:
#     json.dump(examples, file_handle)

In [4]:
with open("config.json", 'r') as file_handle:
    credentials = json.load(file_handle)
    
with open("assetclass.json", 'r') as file_handle:
    codes = json.load(file_handle)

In [6]:
records = {} 
for credential in credentials:
    time.sleep(5)
    driver = webdriver.Chrome('/usr/bin/chromedriver')
    driver.get(credential['url'])
    elem = driver.find_element_by_id("ctl00_body_Login1_txtAccountNumber")
    elem.send_keys(credential['username'])
    elem = driver.find_element_by_id("ctl00_body_Login1_txtPassword")
    elem.send_keys(credential['passwd'])
    elem = driver.find_element_by_id("ctl00_body_sibLogOn")
    elem.click()
    content = driver.page_source
    soup = BeautifulSoup(content)
    driver.close()
    tabulka = soup.find("table", {"class" : "hpc-table w-Positions"})

    rows = tabulka.findAll('tr')
    for tr in rows[1:]:
        cols = tr.findAll('td')
        symbol, qty, last, pchg, value, dchg = [c.text for c in cols]
        qty = float(qty)
        last = float(last)
        value = float("".join([number for number in unidecode.unidecode(value) if number in string.digits or number in "."]))
        if symbol not in records:
            records[symbol] = {}
            records[symbol]['shares'] = qty
            records[symbol]['assetClass'] = codes[symbol]
        else:
            records[symbol]['shares'] += qty

    tabtop = soup.find("table", {"class" : "hpc-table hpc-balances"})
    row = tabtop.find('tr')
    cash = float("".join([number for number in unidecode.unidecode(row.findAll('td')[1].text) if number in string.digits or number in "."]))

    if "CASH" not in records:
        records["CASH"] = cash
    else:
        records["CASH"] += cash

In [22]:
# row = tabtop.find('tr')
# for index, row in enumerate(tabtop.findAll('tr')):
#     print index, row
row2 = tabtop.findAll('tr')[1]
print row2.findAll('td')[1].text
# print row2[0].findAll('td')


$6,209.64

In [ ]:


In [ ]:


In [6]:
class Portfolio(object):
    def __init__(self):
        self.ideal_allocation = {}
        self.stocks_owned = {}
        self.class_total = {}
        self.cash = 0.0
        self.classification = {}
        self.current_asset_percentages = []
        self.core_total = 0.0
        self.total = 0.0
        self.tolerance = 3.5 # percentage off ideal before recommended action
        pass
    
    def get_ideal_allocation(self, infile):
        """Reads in file of ideal portfolio allocation. 
           Use 1-word (no spaces) for asset class. 
           "tolerance" is a special word which gives the tolerance level
            before a rebalance is recommended."""
        with open(infile, 'r') as fh: 
            for line in fh:
                if line.split()[0] == "tolerance":
                    self.tolerance = float(line.split()[1])
                else:
                    self.ideal_allocation[line.split()[0]] = float(line.split()[1])
                    self.class_total[line.split()[0]] = 0.0
    
    def get_account_details(self, infiles):
        for infile in infiles:
            with open(infile, 'r') as fh:
                for line in fh:
                    name = line.split()[0]
                    if name == 'CASH':
                        self.cash += float(line.split()[1].strip("$"))
                    else:
                        if name not in self.stocks_owned:
                            self.stocks_owned[name] = {}
                            self.stocks_owned[name]['shares'] = 0.0
                            self.stocks_owned[name]['shares'] += float(line.split()[1])
                            self.stocks_owned[name]['assetClass'] = line.split()[2]
                        else:
                            self.stocks_owned[name]['shares'] += float(line.split()[1])
                            self.stocks_owned[name]['assetClass'] = line.split()[2]
                            
    def parse_account_details(self, webdict):
        for name in webdict:
            if name == 'CASH':
                self.cash += webdict[name]
            else:
                if name not in self.stocks_owned:
                    self.stocks_owned[name] = {}
                    self.stocks_owned[name]['shares'] = 0.0
                    self.stocks_owned[name]['shares'] += webdict[name]['shares']
                    self.stocks_owned[name]['assetClass'] = webdict[name]['assetClass']
                else:
                    self.stocks_owned[name]['shares'] += webdict[name]['shares']
                    self.stocks_owned[name]['assetClass'] = webdict[name]['assetClass']
                            
    def get_stock_prices(self):
        dataframe = get_quote_yahoo([stock for stock in self.stocks_owned])
        for stock in self.stocks_owned:
            self.stocks_owned[stock]['price'] = dataframe.ix[stock]['last']
    
    def get_core_total(self):
        self.core_total = 0.0
        self.total = 0.0
        self.core_total += self.cash
        self.total += self.cash
        for stock in self.stocks_owned:
            temp_amount = self.stocks_owned[stock]['price'] * self.stocks_owned[stock]['shares']
            if self.stocks_owned[stock]['assetClass'] in self.ideal_allocation:
                self.core_total += temp_amount
                self.class_total[self.stocks_owned[stock]['assetClass']] += temp_amount
                self.total += temp_amount
            else:
                self.total += temp_amount
        pass
    
    def get_current_allocation(self):
        """Remember same stock can't have two assetClasses."""
        for stock in self.stocks_owned:
            if self.stocks_owned[stock]['assetClass'] in self.ideal_allocation:
                temp_asset = self.stocks_owned[stock]['assetClass']
                self.current_asset_percentages.append((stock, self.class_total[temp_asset] / self.core_total * 100. - self.ideal_allocation[temp_asset], temp_asset))
    
    def get_recommendations(self):
        """Print recommendations."""
        print "Recommended actions:"
        for st, perc, asset in sorted(self.current_asset_percentages, key=lambda x: np.abs(x[1]), reverse=True):
            shares = round(self.core_total * perc / 100. / self.stocks_owned[st]['price'], 0)
            if np.abs(perc) >= self.tolerance:
                if shares > 0:
                    print "Sell:", int(np.abs(shares)), st, asset, round(perc,1)
                if shares < 0:
                    print "Buy:", int(np.abs(shares)), st, asset, round(perc,1)
            else:
                print "W/in tol:", 
                if shares > 0.0:
                    print "Sell", int(np.abs(shares)), st, asset, round(perc,1)
                else:
                    print "Buy", int(np.abs(shares)), st, asset, round(perc,1)
        pass
        
    def push_recommendations(self):
        """Pushover recommendations."""
        priority = 0
        return_string = ""
        return_string = '\n'.join([return_string, "Recommended actions:", '\n'])
        for st, perc, asset in sorted(self.current_asset_percentages, key=lambda x: x[1], reverse=True):
            shares = round(self.core_total * perc / 100. / self.stocks_owned[st]['price'], 0)
            if np.abs(perc) >= self.tolerance:
                priority = 1
                if shares > 0:
                    return_string = ' '.join([return_string, "Sell:", str(int(np.abs(shares))), str(st), str(asset), str(round(perc,1)), '\n'])
                if shares < 0:
                    return_string = ' '.join([return_string, "Buy:",  str(int(np.abs(shares))), str(st), str(asset), str(round(perc,1)), '\n'])
            else:
                return_string = ' '.join([return_string, "W/in tol:", ])
                if shares > 0.0:
                    return_string = ' '.join([return_string,  "Sell",  str(int(np.abs(shares))), str(st), str(asset), str(round(perc,1)), '\n'])
                else:
                    return_string = ' '.join([return_string, "Buy",  str(int(np.abs(shares))), str(st), str(asset), str(round(perc,1)), '\n'])
        return return_string, priority
    
    def get_summary(self):
        print "Cash:", locale.currency(self.cash, grouping=True)
        print "Core Total:", locale.currency(self.core_total, grouping=True)
        print "Total:", locale.currency(self.total, grouping=True)
        pass

    def push_summary(self):
        """Pushover summary."""
        return_string = ""
        return_string = ''.join([return_string, "Cash: ", locale.currency(self.cash, grouping=True), "\n"])
        return_string = ''.join([return_string, "Core Total: ", locale.currency(self.core_total, grouping=True), "\n"])
        return_string = ''.join([return_string, "Total: ", locale.currency(self.total, grouping=True), "\n"])
        return return_string
    
    def detailed_summary(self):
        for stock in self.stocks_owned:
            print stock, locale.currency(self.stocks_owned[stock]['price'] * self.stocks_owned[stock]['shares'], grouping=True)
        pass

In [7]:
whatever = Portfolio()
whatever.parse_account_details(records)
whatever.get_ideal_allocation("/Users/jwhitmore/github/RebalanceAssetAllocation/ideal-allocation.txt")
whatever.get_stock_prices()
whatever.get_core_total()
whatever.get_current_allocation()

In [8]:
whatever.get_summary()


Cash: $3,266.55
Core Total: $54,229.34
Total: $76,093.81

In [9]:
whatever.get_recommendations()


Recommended actions:
Sell: 49 RWR RealEstate 7.3
Buy: 59 EFA Foreign -6.9
Sell: 18 IVV Domestic 6.4
Buy: 17 TIP USTIPS -3.6
W/in tol: Sell 3 TLT BndsLng 0.8
W/in tol: Buy 0 SHY BndsShrt -0.0

In [10]:
whatever.detailed_summary()


GOOG $2,886.75
GOOGL $2,938.90
TLT $4,475.26
SHY $4,058.88
AAPL $16,038.82
TIP $6,188.05
RWR $12,069.00
IVV $19,756.00
EFA $4,415.60

In [11]:
records


Out[11]:
{u'AAPL': {'assetClass': u'individual', 'shares': 161.0},
 'CASH': 3266.5499999999997,
 u'EFA': {'assetClass': u'Foreign', 'shares': 70.0},
 u'GOOG': {'assetClass': u'individual', 'shares': 5.0},
 u'GOOGL': {'assetClass': u'individual', 'shares': 5.0},
 u'IVV': {'assetClass': u'Domestic', 'shares': 100.0},
 u'RWR': {'assetClass': u'RealEstate', 'shares': 150.0},
 u'SHY': {'assetClass': u'BndsShrt', 'shares': 48.0},
 u'TIP': {'assetClass': u'USTIPS', 'shares': 55.0},
 u'TLT': {'assetClass': u'BndsLng', 'shares': 38.0}}

In [12]:
codes


Out[12]:
{u'AAPL': u'individual',
 u'EEM': u'Emerging',
 u'EFA': u'Foreign',
 u'GOOG': u'individual',
 u'GOOGL': u'individual',
 u'IVV': u'Domestic',
 u'RWR': u'RealEstate',
 u'SHY': u'BndsShrt',
 u'TIP': u'USTIPS',
 u'TLT': u'BndsLng'}

In [13]:
whatever.current_asset_percentages


Out[13]:
[(u'TLT', 0.75246997289659134, u'BndsLng'),
 (u'SHY', -0.015343170320714528, u'BndsShrt'),
 (u'TIP', -3.5891106179791219, u'USTIPS'),
 (u'RWR', 7.2554801515194534, u'RealEstate'),
 (u'IVV', 6.430463656758505, u'Domestic'),
 (u'EFA', -6.857544274003704, u'Foreign')]

In [ ]: