How to Model Stock Trading Performance with Python

The Problem Statement
The complexity of the modern financial system often requires investors to trade assets across multiple exchanges. This presents a challenge when aggregating and analyzing data spread across multiple platforms.
This implementation seeks to leverage the least amount of data necessary to construct a summative, aggregated time series of short-term stock and cryptocurrency trading activity. From transaction data, the model calculates the final holdings of the portfolio and constructs a historical time series representing the trading performance of the user.
The Implementation
The model is a Ledger class, which allows the user to register transactions to record historical trades and update the current holdings of the ledger. Each transaction requires:
- Type (buy or sell)
- Symbol
- Number of shares purchased
- Purchase date
All transactions are 0 indexed and stored chronologically in a Python dictionary. When a user registers a transaction, the model checks whether the trade was a buy or sell. If the transaction was a buy, it will use any remaining cash to help finance the purchase (profit reinvestment). Then, the model checks whether the user currently owns any shares. If so, these shares are added to the holdings and the average price per share is recalculated. If the trade is a sell, the number of shares sold in the holdings will be subtracted and the total transaction amount is added to the cash holdings.
Finally, the central purpose of the model is to construct a time series representing the performance of the user. The approach of the model is to concatenate time series segments while adjusting historical segments for the introduction of new capital. Each segment represents the portfolio performance for a particular date range. Adjusting the historical time series smooths out the addition of new cash in future periods, more accurately and aesthetically portraying trading performance.
Before beginning the model, I first imported the necessary packages and created two utility functions in order to request live asset prices and historical price data.
import datetime as dt
import numpy as np
import pandas as pd
import pandas_datareader as pdr
from yahoo_fin import stock_info as sidef get_price(stock):
try:
return np.round(si.get_live_price(stock), decimals = 2)
except:
print(f"{stock.upper()} is not a valid ticker")
def get_data(stock, start, end):
try:
stock_data = pdr.data.DataReader(
stock,
"yahoo",
start=start,
end= end
)
return stock_data["Adj Close"]
except pdr._utils.RemoteDataError:
print(f'No data found for {stock}.')
The initial construction of this model begins with the instantiation of the Ledger class, which will store and analyze user transaction data.
class Ledger:
def __init__(self):
self.transactions = {}
self.holdings = {"USD": 0}
def get_holdings(self):
return self.holdings
def get_transactions(self):
return self.transactions
Next, the model must be able to take in transactions from the user. The following function registers the transaction into the queue and updates the current holdings in accordance with the type of trade executed.
def register_transaction(self, transaction, symbol, shares, date):
pps = np.round(
get_data(
symbol,
date,
dt.datetime.now().date().strftime("%Y-%m%d")).values[0],
decimals = 2)
total = pps*shares
transaction_id = len(ledger.get_transactions())
self.transactions.update({
transaction_id: {
"type" : transaction,
"date": date,
"symbol": symbol.upper(),
"shares": shares,
"pps": pps,
"total" : total,
}
})
holdings = self.get_holdings()
if transaction == "buy":
if symbol in holdings.keys():
holding = holdings[symbol]
holdings["USD"] -= total if holdings["USD"] > total
else holdings["USD"]
holding["pps"] = (
(holding["pps"]*holding["shares"] + total) /
(holding["shares"] + shares))
holding["shares"] += shares
else:
holdings.update({
symbol: {
"shares": shares,
"pps": pps
}
})
if transaction == "sell":
holding = holdings[symbol]
holdings["USD"] += total
holding["shares"] -= shares
if holding["shares"] == 0:
del holdings[symbol]
In the main function of the class, the get_ts function constructs a time series segment between each trade date in the portfolio and today. If any additional funds are needed to finance new purchases, the incremental amount of cash necessary to finance the purchase will be broadcasted across the previous values of the time series in order to smooth out holding fluctuations and more accurately represent trading performance.
def get_ts(self):
def get_portfolio(portfolio, start, end):
series = pd.Series(dtype="float64")
for symbol in portfolio.keys():
s = get_data(symbol, start, end) * portfolio[symbol]
if symbol == "USD":
continue
series = series.add(s, fill_value = 0)
return series + portfolio["USD"]
transactions = self.get_transactions()
dates = [transactions[t]["date"] for t in transactions] + [dt.datetime.now().date().strftime("%Y-%m-%d")]
portfolio = {"USD": 0}
series = pd.Series(dtype="float64")
for t in transactions:
tr = transactions[t]
adj = 0
if tr["type"] == "buy":
adj = -1*(tr["total"] if portfolio["USD"] > tr["total"]
else portfolio["USD"])
if tr["symbol"] in portfolio:
portfolio[tr["symbol"]] += tr["shares"]
else:
portfolio.update({tr["symbol"] : tr["shares"]})
if tr["type"] == "sell":
portfolio["USD"] += tr["total"]
portfolio[tr["symbol"]] -= tr["shares"]
add = (tr["total"] - portfolio["USD"]) if tr["total"] > portfolio["USD"] else 0
portfolio["USD"] += adj
s = get_portfolio(portfolio, dates[t], dates[t+1])
series = (series[:s.index[0]] + add).append(s)
return pd.Series([series.loc[x]
if isinstance(series.loc[x], np.float64)
else series.loc[x].values[0] for x in series.index.unique()],
index = series.index.unique())
Testing and Visualization
For demonstration purposes, I created a few transactions to feed into the model. After registering the transactions, I called the get_holdings method to show the portfolio’s final holdings.
ledger = Ledger()ledger.register_transaction("buy", "XOM", 75, "2016-01-01")
ledger.register_transaction("buy", "BAC", 100, "2016-06-01")
ledger.register_transaction("sell", "XOM", 25, "2016-09-01")
ledger.register_transaction("buy", "AAPL", 150, "2017-01-01")
ledger.register_transaction("sell", "BAC", 40, "2017-03-01")
ledger.register_transaction("buy", "CAT", 100, "2017-06-01")
ledger.register_transaction("buy", "MSFT", 25, "2018-01-01")
ledger.register_transaction("sell", "AAPL", 50, "2018-03-01")
ledger.register_transaction("buy", "AMZN", 5, "2018-06-01")
ledger.register_transaction("buy", "IBM", 50, "2019-01-01")
ledger.register_transaction("buy", "TGT", 50, "2019-06-01")
ledger.register_transaction("sell", "AMZN", 3, "2019-09-01")
ledger.register_transaction("buy", "SPY", 20, "2020-03-01")
ledger.register_transaction("sell", "TGT", 10, "2020-09-01")holdings = ledger.get_holdings()
df = pd.DataFrame(holdings).transpose()
df["pps"].loc["USD"] = 1
df["current_price"] = [df.pps.loc["USD"]] + [get_price(x) for x in df.index[1:]]
df["equity"] = df.current_price * df.shares
df["return"] = np.round((df.current_price - df.pps)/ df.pps, decimals = 4)
df
With the help of Plotly we can visualize the holdings of the portfolio.
import plotly.graph_objects as gofig = go.Figure(data = [
go.Pie(
values = df.equity,
labels = df.index,
textposition = "inside",
textinfo = "percent+label",
textfont_color = "#111111",
)])fig.update_layout(
template = "plotly_dark",
title = {
"text" : "Portfolio Holdings",
"y" : .875,
"x" : .05,
"xanchor" : "left",
"yanchor" : "bottom"
},
margin = dict(l = 60, t = 40, r = 60, b = 40),
legend = dict(
yanchor = "middle",
y = .5,
xanchor = "left",
x = .875)
)fig.show()
Based on the current and original price per share, we can also construct a radar chart to compare the relative returns of each investment.
fig = go.Figure(data = go.Scatterpolar(
r = df["return"][1:].values,
theta = df.index[1:],
fill = 'toself',
))fig.update_layout(
polar = dict(radialaxis = dict(visible = True)),
margin = dict(l = 40, t = 40, r = 40, b = 40),
template = "plotly_dark",
title = {
"text" : "Individual Asset Returns",
"y" : .875,
"x" : .05,
"xanchor" : "left",
"yanchor" : "bottom"
},
)fig.show()
Since the model stores transaction activity, we can also analyze and visualize the transaction data.
transactions = ledger.get_transactions()
df = pd.DataFrame(transactions).transpose()
df.loc[df.type == "sell", "total"] = df.loc[df.type == "sell", "total"] * -1
df
For the visualization, I changed the total value of all sales to negative. This will make sense in the context of the visualization.
fig = go.Figure([
go.Bar(
x = df.symbol,
y = df.total,
text = df.date,
meta = df.shares,
customdata = df.pps,
marker_color= ["green" if x == "buy" else "red" for x in df.type],
hovertemplate = ("Total: $%{y:,.2f}" +
"<br>Shares: %{meta}" +
"<br>PPS: $%{customdata:,.2f}" +
"<br>Date: %{text}"))
],
)fig.update_layout(
template = "plotly_dark",
title = {
"text" : "Trading Activity",
"y" : .9,
"x" : .5,
"xanchor" : "center",
"yanchor" : "middle",
},
height = 400,
margin = dict(t = 60, l = 60, r = 40, b = 40)
)fig.show()
Finally, we will use the model to construct the time series portraying the user’s trading performance. To benchmark the portfolio, we will compare its standardized performance with the S&P 500.
ts = ledger.get_ts()spy = get_data("SPY", ts.index[0], ts.index[-1])df = pd.DataFrame({"Portfolio" : ts.values, "SPY" : spy[ts.index[0]:].values}, index = ts.index)
df.Portfolio = np.round((df.Portfolio - df.Portfolio[0]) / df.Portfolio[0], decimals = 4)
df.SPY = np.round((df.SPY - df.SPY[0]) / df.SPY[0], decimals = 4)fig = go.Figure()fig.add_trace(go.Scatter(
x = df.index,
y = df.Portfolio,
mode = "lines",
name = "Portfolio"
))fig.add_trace(go.Scatter(
x = df.index,
y = df.SPY,
mode = "lines",
name = "SPY"
))fig.update_layout(
title = {
"text" : f'Portfolio Performance',
"y": .9,
"x": .05,
"xanchor": "left",
"yanchor": "middle"
},
template = "plotly_dark",
margin = dict(l = 60, t = 80, r = 60, b = 60),
height = 500
)fig.show()
Conclusion
In conclusion, the model can accurately and programmatically store and analyze transaction data to record and visualize stock trading performance. The entire Jupyter Notebook can be viewed on my GitHub. This model could be improved with the addition of a more in-depth analysis of trading performance, portfolio construction, and risk-adjusted return. Additionally, this model could (and perhaps may) be paired with a machine learning model to backtest stock trading strategies or financial algorithms.
More content at plainenglish.io