diff --git a/README.md b/README.md index 7f44b55..ac5ef46 100644 --- a/README.md +++ b/README.md @@ -112,21 +112,49 @@ The project uses a trading date observation window, which sets 4 portfolio rebal First, update the stock database: ```bash - stocksense --update + stocksense update ``` ### Model Training -Train the model for a given trade date: +Train the model for a specific trade date: ```bash - stocksense --train --trade-date YYYY-MM-DD + stocksense train --trade-date YYYY-MM-DD ``` + For example: + ```bash + stocksense train --trade-date 2024-03-01 + ``` + Note: Trade date must be the 1st of March, June, September, or December. + + +Generate an investment portfolio: + ```bash + stocksense portfolio --trade-date YYYY-MM-DD [--weighting STRATEGY] [--n-stocks N] + ``` + + Options: + - `--weighting`: Portfolio weighting strategy (`market_cap` or `equal`). Default: `market_cap` + - `--n-stocks`: Number of stocks to include in portfolio. Default: 30 -Score stocks for a given trade date: + Example: ```bash - stocksense --score --trade-date YYYY-MM-DD + # Generate a market-cap weighted portfolio with 25 stocks + stocksense portfolio --trade-date 2024-03-01 --weighting market_cap --n-stocks 25 + + # Generate an equal-weighted portfolio with default 30 stocks + stocksense portfolio --trade-date 2024-03-01 --weighting equal ``` +#### Command Reference + +```bash +stocksense --help # Show available commands +stocksense update --help # Show help for update command +stocksense train --help # Show help for train command +stocksense portfolio --help # Show help for portfolio command +``` + In order to evaluate for the last trading date, don't specify a trade date. ### Streamlit App diff --git a/notebooks/portfolio_analysis.ipynb b/notebooks/portfolio_analysis.ipynb new file mode 100644 index 0000000..0652dea --- /dev/null +++ b/notebooks/portfolio_analysis.ipynb @@ -0,0 +1,78 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import polars as pl\n", + "\n", + "DATE = \"2023-12-01\"\n", + "REPORT_DIR = Path(\"../reports/scores\")\n", + "PORTFOLIO_DIR = Path(\"../reports/portfolios\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Portfolio: ../reports/portfolios/portfolio_2023-06-01.xlsx\n", + "Average return: 32.73% (80.0)\n", + "\n", + "Portfolio: ../reports/portfolios/portfolio_2023-03-01.xlsx\n", + "Average return: 59.51% (100.0)\n", + "\n", + "Portfolio: ../reports/portfolios/portfolio_2023-12-01.xlsx\n", + "Average return: 21.08% (80.0)\n" + ] + } + ], + "source": [ + "for portfolio in PORTFOLIO_DIR.glob(\"portfolio_*.xlsx\"):\n", + " df = pl.read_excel(portfolio, sheet_name=\"Full Portfolio\")\n", + " top = df.head(5)\n", + "\n", + " if \"fwd_return_4Q\" not in df.columns:\n", + " continue\n", + "\n", + " top_freturn = top.select(pl.col(\"fwd_return_4Q\")).mean().item()\n", + " top_hits = top.select(pl.col(\"fwd_return_4Q\") > 0).sum().item()\n", + "\n", + " top_hitrate = (top_hits / len(top)) * 100\n", + "\n", + " print(f\"\\nPortfolio: {portfolio}\")\n", + " print(f\"Average return: {top_freturn:.2f}% ({top_hitrate:.1f})\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/report_analysis.ipynb b/notebooks/report_analysis.ipynb deleted file mode 100644 index e956bb2..0000000 --- a/notebooks/report_analysis.ipynb +++ /dev/null @@ -1,172 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "import polars as pl\n", - "\n", - "from stocksense.database import DatabaseHandler\n", - "\n", - "DATE = \"2023-12-01\"\n", - "REPORT_DIR = Path(\"../reports/scores\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32m2024-12-14 16:38:06.574\u001b[0m | \u001b[32m\u001b[1mSUCCESS \u001b[0m | \u001b[36mstocksense.database.schema\u001b[0m:\u001b[36mcreate_tables\u001b[0m:\u001b[36m121\u001b[0m - \u001b[32m\u001b[1mTables created successfully\u001b[0m\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "shape: (461, 16)
ticadj_closepesaleq_yoyfwd_returnexcess_returnrisk_returnpred_risk_return_3Qrank_risk_return_3Qpred_risk_return_4Qrank_risk_return_4Qavg_scorenamesectordate_addeddate_removed
strf64f64f64f64f64f64f64i64f64i64f64strstrdatedate
"SEE"33.48750715.771561-1.3281926.042746-13.1003732.8865735.658571299.60338488.5"Sealed Air""Materials"null2023-12-18
"WBA"19.217991-5.82989.16207-38.549164-57.692283-14.6405175.6297507119.5335971814.5"Walgreens Boots Alliance""Consumer Staples"1979-12-31null
"FMC"53.69709814.045465-28.7031668.275038-10.8680822.7310695.66219489.5115672315.5"FMC Corporation""Materials"2009-08-19null
"PSA"255.41491723.0929565.11785816.228237-2.91488210.8448265.594103259.599665917.0"Public Storage""Real Estate"2005-08-19null
"LUV"26.06261331.947774.9035373.950928-15.1921911.5467875.621783159.5217342118.0"Southwest Airlines""Industrials"1994-07-01null
"NOW"690.78997889.31269824.95903911.744603-7.3985165.5501154.97487834328.223453452442.0"ServiceNow""Information Technology"2019-11-21null
"NVDA"46.75085161.055955200.0158.87167100.052.0311334.97223044338.100908456444.5"Nvidia""Information Technology"2001-11-30null
"ANET"216.63999935.44291628.26773655.72242936.5793122.9024734.847444488.336229443445.5"Arista Networks""Information Technology"2018-08-28null
"LVS"45.99071953.400266178.109453-10.031928-29.175048-5.3663874.92775964407.871676460450.0"Las Vegas Sands""Consumer Discretionary"2019-10-03null
"NTAP"89.58324427.172948-6.07336140.74200921.5988921.4617244.84075934497.989794459454.0"NetApp""Information Technology"1999-06-25null
" - ], - "text/plain": [ - "shape: (461, 16)\n", - "┌──────┬────────────┬───────────┬────────────┬───┬────────────┬────────────┬───────────┬───────────┐\n", - "│ tic ┆ adj_close ┆ pe ┆ saleq_yoy ┆ … ┆ name ┆ sector ┆ date_adde ┆ date_remo │\n", - "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ d ┆ ved │\n", - "│ str ┆ f64 ┆ f64 ┆ f64 ┆ ┆ str ┆ str ┆ --- ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ date ┆ date │\n", - "╞══════╪════════════╪═══════════╪════════════╪═══╪════════════╪════════════╪═══════════╪═══════════╡\n", - "│ SEE ┆ 33.487507 ┆ 15.771561 ┆ -1.328192 ┆ … ┆ Sealed Air ┆ Materials ┆ null ┆ 2023-12-1 │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 8 │\n", - "│ WBA ┆ 19.217991 ┆ -5.8298 ┆ 9.16207 ┆ … ┆ Walgreens ┆ Consumer ┆ 1979-12-3 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ Boots ┆ Staples ┆ 1 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ Alliance ┆ ┆ ┆ │\n", - "│ FMC ┆ 53.697098 ┆ 14.045465 ┆ -28.703166 ┆ … ┆ FMC Corpor ┆ Materials ┆ 2009-08-1 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ ation ┆ ┆ 9 ┆ │\n", - "│ PSA ┆ 255.414917 ┆ 23.092956 ┆ 5.117858 ┆ … ┆ Public ┆ Real ┆ 2005-08-1 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ Storage ┆ Estate ┆ 9 ┆ │\n", - "│ LUV ┆ 26.062613 ┆ 31.94777 ┆ 4.903537 ┆ … ┆ Southwest ┆ Industrial ┆ 1994-07-0 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ Airlines ┆ s ┆ 1 ┆ │\n", - "│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │\n", - "│ NOW ┆ 690.789978 ┆ 89.312698 ┆ 24.959039 ┆ … ┆ ServiceNow ┆ Informatio ┆ 2019-11-2 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ n ┆ 1 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ Technology ┆ ┆ │\n", - "│ NVDA ┆ 46.750851 ┆ 61.055955 ┆ 200.0 ┆ … ┆ Nvidia ┆ Informatio ┆ 2001-11-3 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ n ┆ 0 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ Technology ┆ ┆ │\n", - "│ ANET ┆ 216.639999 ┆ 35.442916 ┆ 28.267736 ┆ … ┆ Arista ┆ Informatio ┆ 2018-08-2 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ Networks ┆ n ┆ 8 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ Technology ┆ ┆ │\n", - "│ LVS ┆ 45.990719 ┆ 53.400266 ┆ 178.109453 ┆ … ┆ Las Vegas ┆ Consumer ┆ 2019-10-0 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ Sands ┆ Discretion ┆ 3 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ ary ┆ ┆ │\n", - "│ NTAP ┆ 89.583244 ┆ 27.172948 ┆ -6.073361 ┆ … ┆ NetApp ┆ Informatio ┆ 1999-06-2 ┆ null │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ n ┆ 5 ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ Technology ┆ ┆ │\n", - "└──────┴────────────┴───────────┴────────────┴───┴────────────┴────────────┴───────────┴───────────┘" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "stock_info = DatabaseHandler().fetch_stock()\n", - "df = pl.read_csv(REPORT_DIR / f\"scores_{DATE}.csv\")\n", - "df = df.join(stock_info, on=\"tic\", how=\"left\")\n", - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "DATE 2023-12-01\n", - "Average top return: 4.36% (70.0)\n", - "Average bottom return: 10.92% (82.5)\n" - ] - } - ], - "source": [ - "n = 40\n", - "top = df.head(n)\n", - "bottom = df.tail(n)\n", - "\n", - "top_freturn = top.select(pl.col(\"risk_return\")).mean().item()\n", - "bottom_freturn = bottom.select(pl.col(\"risk_return\")).mean().item()\n", - "\n", - "# Calculate hit rates (% of stocks with positive returns)\n", - "top_hits = top.select(pl.col(\"risk_return\") > 0).sum().item()\n", - "bottom_hits = bottom.select(pl.col(\"risk_return\") > 0).sum().item()\n", - "\n", - "top_hitrate = (top_hits / n) * 100\n", - "bottom_hitrate = (bottom_hits / n) * 100\n", - "\n", - "\n", - "print(f\"\\nDATE {DATE}\")\n", - "print(f\"Average top return: {top_freturn:.2f}% ({top_hitrate:.1f})\")\n", - "print(f\"Average bottom return: {bottom_freturn:.2f}% ({bottom_hitrate:.1f})\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.345" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "round(0.34539, 3)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.0" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/stocksense/model/portfolio.py b/stocksense/model/portfolio.py index 4a7804d..10865d6 100644 --- a/stocksense/model/portfolio.py +++ b/stocksense/model/portfolio.py @@ -98,10 +98,9 @@ def _filter_candidates(self, df: pl.DataFrame) -> pl.DataFrame: quality_filters = ( (pl.col("pe") > 0) - & (pl.col("ev_ebitda") < 50) & (pl.col("saleq_yoy") > -20) & (pl.col("fcf_yoy") > -50) - & (pl.col("price_mom") > -20) + & (pl.col("price_mom") > -25) ) return df.filter(quality_filters)