Track your personal stock portfolios with real-time data.
Automatically determine your total gain, and track and compare historical performance.
import plotly.express as px
from capon.portfolio import Portfolio, Lot
from capon.visualization import plot_status
my_portfolio = Portfolio([
Lot('2020-03-20', 'AMZN', 2, 1888.86 / 20),
Lot('2020-03-20', 'TSLA', 8, 451.40 / 5),
Lot('2020-03-23', 'GOOGL', 3, 1037.89),
Lot('2020-03-23', 'AMC', 1041, 2.88),
Lot('2020-03-27', 'ZM', 20, 150.29),
])
my_portfolio.trades
symbol | quantity | price_buy | active | timestamp_buy | cost | lot_index | timestamp_sell | price_sell | |
---|---|---|---|---|---|---|---|---|---|
0 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None |
1 | TSLA | 8 | 90.280 | 1 | 2020-03-20 | 722.240 | 1 | NaT | None |
2 | AMC | 1041 | 2.880 | 1 | 2020-03-23 | 2998.080 | 2 | NaT | None |
3 | GOOGL | 3 | 1037.890 | 1 | 2020-03-23 | 3113.670 | 3 | NaT | None |
4 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None |
def color_sign(val):
color = 'red' if val<0 else 'green' if val>0 else 'black'
return f'color: {color}'
status = my_portfolio.status()
display(status
.style
.format('{:,.2f}', subset=['price_buy', 'cost', 'price', 'value'])
.format({'gain_pct': '{:+.2%}', 'gain': '{:+,.0f}'})
.applymap(color_sign, subset=['gain_pct', 'gain'])
)
total_cost, total_value = status.sum()[['cost', 'value']]
print(f'Total cost: {total_cost:,.2f}; Market value: {total_value:,.2f}')
print(f'Total gain: {total_value-total_cost:+,.2f} ({total_value/total_cost-1:+,.2%})')
0%| | 0/5 [00:00<?, ?it/s]
symbol | quantity | price_buy | active | timestamp_buy | cost | lot_index | timestamp | price | value | gain | gain_pct | daily_pct | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | AMZN | 2 | 94.443000 | 1 | 2020-03-20 00:00:00 | 188.886000 | 0 | 2023-01-11 00:00:00 | 95.089996 | 190.179993 | +1 | +0.69% | 0.058084 |
1 | TSLA | 8 | 90.280000 | 1 | 2020-03-20 00:00:00 | 722.240000 | 1 | 2023-01-11 00:00:00 | 123.220001 | 985.760010 | +264 | +36.49% | 0.036769 |
2 | AMC | 1041 | 2.880000 | 1 | 2020-03-23 00:00:00 | 2998.080000 | 2 | 2023-01-11 00:00:00 | 4.920000 | 5121.720079 | +2,124 | +70.83% | 0.211823 |
3 | GOOGL | 3 | 1037.890000 | 1 | 2020-03-23 00:00:00 | 3113.670000 | 3 | 2023-01-11 00:00:00 | 91.519997 | 274.559990 | -2,839 | -91.18% | 0.035060 |
4 | ZM | 20 | 150.290000 | 1 | 2020-03-27 00:00:00 | 3005.800000 | 4 | 2023-01-11 00:00:00 | 70.010002 | 1400.200043 | -1,606 | -53.42% | -0.011995 |
Total cost: 10,028.68; Market value: 7,972.42 Total gain: -2,056.26 (-20.50%)
/var/folders/16/wgtxm6md5191yqyf_pw79yj40000gn/T/ipykernel_83289/1204018774.py:13: FutureWarning: Dropping of nuisance columns in DataFrame reductions (with 'numeric_only=None') is deprecated; in a future version this will raise TypeError. Select only valid columns before calling the reduction. total_cost, total_value = status.sum()[['cost', 'value']]
plot_status(status)
/Users/eyalgal/Dropbox/PhD/Code/capon/capon/visualization/portfolio.py:9: FutureWarning: Dropping of nuisance columns in DataFrame reductions (with 'numeric_only=None') is deprecated; in a future version this will raise TypeError. Select only valid columns before calling the reduction. total_cost, total_value = status.sum()[['cost', 'value']]
performance = my_portfolio.performance()
display(performance)
0%| | 0/5 [00:00<?, ?it/s]
symbol | quantity | price_buy | active | timestamp_buy | cost | lot_index | timestamp_sell | price_sell | timestamp | price | value | gain | gain_pct | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None | 2020-03-20 | 92.304497 | 184.608994 | -4.277006 | -0.022643 |
2 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None | 2020-03-23 | 95.141502 | 190.283005 | 1.397005 | 0.007396 |
3 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None | 2020-03-24 | 97.004997 | 194.009995 | 5.123995 | 0.027127 |
4 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None | 2020-03-25 | 94.292000 | 188.584000 | -0.302000 | -0.001599 |
5 | AMZN | 2 | 94.443 | 1 | 2020-03-20 | 188.886 | 0 | NaT | None | 2020-03-26 | 97.774498 | 195.548996 | 6.662996 | 0.035275 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
3545 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None | 2023-01-05 | 65.620003 | 1312.400055 | -1693.399945 | -0.563377 |
3546 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None | 2023-01-06 | 69.510002 | 1390.200043 | -1615.599957 | -0.537494 |
3547 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None | 2023-01-09 | 70.080002 | 1401.600037 | -1604.199963 | -0.533701 |
3548 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None | 2023-01-10 | 70.860001 | 1417.200012 | -1588.599988 | -0.528512 |
3549 | ZM | 20 | 150.290 | 1 | 2020-03-27 | 3005.800 | 4 | NaT | None | 2023-01-11 | 70.010002 | 1400.200043 | -1605.599957 | -0.534167 |
3538 rows × 14 columns
fig = px.line(performance, x='timestamp', y='gain_pct', color='symbol',
title='Stocks performance',
template='capon')
fig.update_layout(yaxis_tickformat='+.0%')
fig.update_traces(hovertemplate=None)
performance_total = (
performance
.assign(date=lambda df: df['timestamp'].dt.date)
.groupby('date')[['cost', 'value']].sum()
.assign(
gain=lambda df: df['value']-df['cost'],
gain_pct=lambda df: df['value']/df['cost']-1
)
)
fig = px.line(performance_total.reset_index(), x='date', y='gain_pct',
title='Portfolio performance',
height=400, template='capon')
fig.update_layout(yaxis_tickformat='+.0%')
fig.update_traces(mode='lines+markers')
display(fig)
(
performance_total
.assign(gain_pct_diff=lambda df: df['gain_pct'].diff())
.tail(10)
.style.format('{:,.2f}').format({'gain':'{:+,.0f}', 'gain_pct':'{:+.2%}', 'gain_pct_diff':'{:+.2%}'})
.applymap(color_sign, subset=['gain', 'gain_pct', 'gain_pct_diff'])
)
cost | value | gain | gain_pct | gain_pct_diff | |
---|---|---|---|---|---|
date | |||||
2022-12-28 | 10028.676000 | 6628.019905 | -3,401 | -33.91% | -1.77% |
2022-12-29 | 10028.676000 | 7078.009850 | -2,951 | -29.42% | +4.49% |
2022-12-30 | 10028.676000 | 7009.800148 | -3,019 | -30.10% | -0.68% |
2023-01-03 | 10028.676000 | 6728.130138 | -3,301 | -32.91% | -2.81% |
2023-01-04 | 10028.676000 | 6954.330189 | -3,074 | -30.66% | +2.26% |
2023-01-05 | 10028.676000 | 6742.320062 | -3,286 | -32.77% | -2.11% |
2023-01-06 | 10028.676000 | 6736.709917 | -3,292 | -32.83% | -0.06% |
2023-01-09 | 10028.676000 | 6889.670070 | -3,139 | -31.30% | +1.53% |
2023-01-10 | 10028.676000 | 7039.459940 | -2,989 | -29.81% | +1.49% |
2023-01-11 | 10028.676000 | 7972.420115 | -2,056 | -20.50% | +9.30% |