def __init__( self, path: str = None, data: pd.DataFrame = pd.DataFrame(), holdings: pd.DataFrame = pd.DataFrame(), ): """Initialize the portfolio with holdings and market data. Args: path: The name of an a directory containing holdings and market data. If this is None then the portfolio must be described by the data and holdings arguments. data: A DataFrame containing market close data for a set of financial instruments over a period of time. holdings: A DataFrame containing the number of shares held of the set of symbols in data over a time period corresponding to that of data. """ if len(data) > 0 and len(holdings) > 0: logger.debug("Data and holdings arguments are set.") self.data = data self.holdings = holdings else: logger.debug("Data and holdings are not set.") if path is None: path = Path().home() / ".portfolio" / "data" self.path = Path(path) if not self.path.is_dir(): logger.info("%s is not a directory, creating it...", self.path) self.path.mkdir() self.data_file = self.path / "prices.feather" self.holdings_file = self.path / "holdings.feather" try: # The feather format does not support date indices, # so set the Date colemn to be the index. self.holdings = pd.read_feather( self.holdings_file).set_index("date") except FileNotFoundError: logger.info("No stored holdings found.") self.holdings = holdings symbols = self.holdings.columns try: # The feather format does not support date indices, # so set the Date colemn to be the index. self.data = pd.read_feather(self.data_file).set_index("date") except FileNotFoundError: logger.info("Market data is not stored .") calendar = USFederalHolidayCalendar() today = pd.Timestamp.today().normalize() last_business_day = min(today - BDay(1), self.data.index.max() + BDay(1)) holidays = calendar.holidays(last_business_day, today) if (today not in self.data.index and last_business_day not in self.data.index and holidays.empty): self.data = self.get_market_data( symbols, start=last_business_day, end=today, ) self.holdings = self.holdings.reindex(self.data.index, method="ffill").dropna()
def plot(self, period: str = "w") -> str: """Plot the portfolio total and save to an image file. Args: period: The period of time to plot, defaults to weeks. Returns: Name of the file containing the plotted data. """ register_matplotlib_converters() pf = self.pf fig, ax = plt.subplots(figsize=(8, 6)) pf.value.Total.resample(period).plot.line( ax=ax, color="blue", title="Portfolio Summary", ylabel="Value", ylim=(pf.value.Total.min(), pf.value.Total.max()), ) plt.grid(True) with tempfile.NamedTemporaryFile(dir=pf.path, prefix="portfolio_", suffix=".png", delete=False) as file: logger.debug("Saving portfolio chart to %s...", file.name) plt.savefig(file.name, bbox_inches="tight") return file.name
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) try: self.from_json() logger.debug("Read config from json=%s", self.json_config) except Exception as e: logger.exception(e)
def main() -> None: """Entry point for the portfolio app.""" logger.debug('Running "%s" in "%s"', " ".join(sys.argv), Path(".").resolve()) with Portfolio() as portfolio: args = make_parser().parse_args() logger.debug("Arguments parsed as %s", args) if hasattr(args, "func"): with PortfolioConfig() as config: args.func(args, portfolio, config)
def __exit__(self, type, value, traceback): """Stores market and holdings data to an feather file.""" saved_data = pd.read_feather(self.data_file) saved_holdings = pd.read_feather(self.holdings_file) if not saved_data.equals(self.data.reset_index()): logger.debug("Writing market data to %s...", self.data_file) self.data.reset_index().to_feather(self.data_file) if not saved_holdings.equals(self.holdings.reset_index()): logger.debug("Writing holdings to %s...", self.holdings_file) self.holdings.reset_index().to_feather(self.holdings_file)
def email(self, test: bool = False) -> bool: """Send the portfolio report by email. Args: test: True if the email should only be prepared and printed but not sent. Returns: True if an email was actually sent, False otherwise. """ date = self.date with PortfolioConfig() as config: try: server = config["email"]["smtp_server"] port = config["email"]["smtp_port"] user = config["email"]["smtp_user"] password = config["email"]["smtp_password"] sender = config["email"]["sender"] recipients = config["email"]["recipients"] except KeyError: logger.exception("Email configuration incomplete.") return False message = MIMEMultipart() message["From"] = sender message["Reply-To"] = sender message["To"] = ", ".join(recipients) message["Message-ID"] = make_msgid(domain="gmail.com") message["Date"] = formatdate(localtime=True) message["Subject"] = "Portfolio Report" content = MIMEMultipart("alternative") part1 = MIMEText(self.text, "plain", "us-ascii") content.attach(part1) part2 = MIMEText(self.html, "html", "us-ascii") content.attach(part2) message.attach(content) if date.dayofweek == 4: chart1 = MIMEImage(open(self.data["chart_file"], "rb").read()) chart1.add_header("Content-Disposition", "attachment", filename="portfolio.png") chart1.add_header("X-Attachment_Id", "0") chart1.add_header("Content-Id", "portfolio-summary") message.attach(chart1) if test: logger.debug("Testing only; email not sent.") print(message.as_string()) return False with smtplib.SMTP(server, int(port)) as smtp: smtp.ehlo() smtp.starttls() smtp.login(user, password) smtp.send_message(message) return True
def list(args, portfolio, config=None): logger.debug("Listing holdings...") if args.verbosity < 1: listing = "\t".join(portfolio.holdings.columns) elif args.verbosity == 1: listing = portfolio.holdings.iloc[-1] else: listing = pd.DataFrame( index=portfolio.holdings.columns, columns=["Holdings", "Price", "Value"], data={ "Holdings": portfolio.holdings.iloc[-1], "Price": portfolio.data.iloc[-1], "Value": portfolio.value.drop(columns="Total").iloc[-1], }, ) print(listing) return listing
def export(self, filename: str = "holdings.csv") -> bool: """Export the holdings in the portfolio to a file. Args: filename: A CSV or XLSX file where holdings data should be exported. """ logger.debug("Exporting data to %s...", filename) if filename.endswith(".csv"): self.holdings.drop_duplicates().to_csv(filename) return True elif filename.endswith(".xlsx"): with pd.ExcelWriter(filename, datetime_format="mm/dd/yyyy") as writer: self.data.to_excel(writer, sheet_name="Prices") self.holdings.drop_duplicates().to_excel(writer, sheet_name="Holdings") self.value.to_excel(writer, sheet_name="Value") return True return False
def add_symbol(self, symbol: str, quantity: float, date: pd.Timestamp) -> None: """Add a new symbol to the portfolio. :param symbol: A ticker symbol. :param quantity: Initial quantity of shares of symbol. :param date: The shares must be added on a date. :raises KeyError: The symbol is already in the portfolio. """ if symbol in self.holdings.columns: raise KeyError( f"{symbol} is already inportfolio. Use add_shares or add_cash instead." ) self.holdings.loc[date:, symbol] = quantity try: self.data[symbol] = self.get_market_data(list(symbol)) except KeyError: logger.debug("Unable to retrieve market data for %s.", symbol)
def update(args, portfolio, config): logger.debug("Updating portfolio...") if args.add: args.add[1] = float(args.add[1]) args.add[2] = pd.Timestamp(args.add[2]) if args.cash: row = portfolio.add_cash(*args.add) else: row = portfolio.add_shares(*args.add) elif args.remove: args.remove[1] = float(args.remove[1]) args.remove[2] = pd.Timestamp(args.remove[2]) if args.cash: row = portfolio.remove_cash(*args.remove) else: row = portfolio.remove_shares(*args.remove) elif args.set: args.set[1] = float(args.set[1]) args.set[2] = pd.Timestamp(args.set[2]) row = portfolio.set_shares(*args.set) logger.debug("Portfolio updated with %s.", row) return args
def report(args, portfolio, config): logger.debug("Running report...") report = Report(portfolio, config=config, date=args.date) if hasattr(args, "email") and args.email: logger.debug("Emailing report...") report.email(args.test) if args.verbosity > 0: print(report.text) if args.output_file: logger.debug("Writing report to %s...", args.output_file.name) if splitext(args.output_file.name)[1] == ".txt": args.output_file.write(report.text) if splitext(args.output_file.name)[1] == ".html": args.output_file.write(report.html) args.output_file.close() if args.export_file: portfolio.export(args.export_file.name)
from pathlib import Path import sys from portfolio.config import PortfolioConfig from portfolio.log import logger from portfolio.portfolio import Portfolio from portfolio.cli import make_parser def main() -> None: """Entry point for the portfolio app.""" logger.debug('Running "%s" in "%s"', " ".join(sys.argv), Path(".").resolve()) with Portfolio() as portfolio: args = make_parser().parse_args() logger.debug("Arguments parsed as %s", args) if hasattr(args, "func"): with PortfolioConfig() as config: args.func(args, portfolio, config) if __name__ == "__main__": try: main() except KeyboardInterrupt: logger.debug("Exiting by keyboard interrupt...") except Exception: logger.exception("Fatal error in main.") raise logger.debug("%s exited.", __name__)
def interactive(args, portfolio, config): logger.debug("Running in interactive mode...") _Interactive(portfolio, args) return args