Example #1
0
    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()
Example #2
0
    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
Example #3
0
 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)
Example #4
0
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)
Example #5
0
 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)
Example #6
0
    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
Example #7
0
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
Example #8
0
    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
Example #9
0
    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)
Example #10
0
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
Example #11
0
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)
Example #12
0
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__)
Example #13
0
def interactive(args, portfolio, config):
    logger.debug("Running in interactive mode...")
    _Interactive(portfolio, args)
    return args