├── .env.sample ├── .gitignore ├── LICENSE ├── README.md ├── compose.yaml ├── nbs ├── 1 - Hello World Load dotenv.ipynb ├── 10 - Load Stock Quotes into Django.ipynb ├── 11 - Batch and Bulk Load Stock Quotes.ipynb ├── 12 - Stock Sync.ipynb ├── 2 - Hello World Polygon.ipynb ├── 3 - Hello World Alpha Vantage.ipynb ├── 4 - Transform Polygon Response.ipynb ├── 5 - Transform Alpha Vantage Response.ipynb ├── 6 - Polygon API Client.ipynb ├── 7 - Alpha Vantage API Client.ipynb ├── 8 - Django Setup.ipynb ├── 9 - Helper API Clients.ipynb ├── Analyze - 1 - Moving Averages.ipynb ├── Analyze - 2 - Daily Moving Averages.ipynb ├── Analyze - 3 - Volume Trend and Price Target.ipynb ├── Analyze - 4 - Analyze Stocks - Relative Strength Index.ipynb ├── Analyze - 5 - Stocks Services.ipynb ├── Decide - 1 - Logic-based Recommendation.ipynb ├── Decide - 2 - LLM-based Recommendation.ipynb ├── Putting it all together.ipynb └── setup.py ├── requirements.txt └── src ├── cfehome ├── __init__.py ├── asgi.py ├── celery.py ├── settings.py ├── urls.py └── wsgi.py ├── helpers ├── __init__.py └── clients │ ├── __init__.py │ ├── _alpha_vantage.py │ └── _polygon.py ├── manage.py └── market ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py ├── 0002_alter_stockquote_volume_weighted_average.py ├── 0003_alter_stockquote_managers.py ├── 0004_alter_stockquote_unique_together.py ├── 0005_stockquote_raw_timestamp.py └── __init__.py ├── models.py ├── services.py ├── tasks.py ├── tests.py ├── utils.py └── views.py /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5431/postgres" 2 | REDIS_URL="redis://localhost:6878" 3 | 4 | # API services 5 | ALPHA_VANTAGE_API_KEY="" 6 | POLOGYON_API_KEY="" 7 | OPENAI_API_KEY="" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Coding For Entrepreneurs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stock Trading Bot 2 | [![Star this repo](https://img.shields.io/github/stars/codingforentrepreneurs/Stock-Trading-Bot?style=social)](https://github.com/codingforentrepreneurs/Stock-Trading-Bot) 3 | 4 | Learn how to extract data, analyze, and decide on stocks in the market using Django, Celery, TimescaleDB, Jupyter, OpenAI, and more. 5 | 6 | Thanks to [Timescale](https://kirr.co/eedxyv) for partnering with me on this tutorial. 7 | 8 | 9 | __Tech Stack__ 10 | - [Python 3.12](https://github.com/python) 11 | - [Django](https://github.com/django/django) (`pip install "Django>=5.1,<5.2"`) 12 | - [TimescaleDB Cloud](https://tsdb.co/justin) (or Docker version) 13 | - [Django Timescaledb](https://github.com/jamessewell/django-timescaledb) (`pip install django-timescaledb`) 14 | - [Python requests](https://github.com/psf/requests) (`pip install requests`) 15 | - [Jupyter](https://jupyter.org/) (`pip install jupyter`) 16 | - [Psycopg Binary Release](https://pypi.org/project/psycopg/) (`pip install "psycopg[binary]"`) 17 | - [Python Decouple](https://github.com/HBNetwork/python-decouple) to load environment variables (e.g. `.env`) with type casting and default values. 18 | - [Polygon.io](https://polygon.io/?utm_source=cfe&utm_medium=github&utm_campaign=cfe-github) ([docs](https://polygon.io/docs/stocks/getting-started?utm_source=cfe&utm_medium=github&utm_campaign=cfe-github)) 19 | - [Alpha Vantage]( https://www.alphavantage.co/?utm_source=cfe&utm_medium=github&utm_campaign=cfe-github) ([docs](https://www.alphavantage.co/documentation/?utm_source=cfe&utm_medium=github&utm_campaign=cfe-github)) 20 | - [OpenAI]( https://www.openai.com/?utm_source=cfe&utm_medium=github&utm_campaign=cfe-github) 21 | 22 | ## Tutorial 23 | - In-depth setup [on YouTube (https://youtu.be/aApDye1TWJ4)](https://youtu.be/aApDye1TWJ4) 24 | - [Django Setup for use in Jupyter Notebooks (short + code)](https://www.codingforentrepreneurs.com/shorts/django-setup-for-use-in-jupyter-notebooks) 25 | - Full tutorial [on YouTube (https://youtu.be/O3O1z5hTdUM)](https://youtu.be/O3O1z5hTdUM) 26 | 27 | ## Getting Started 28 | 29 | Download the following: 30 | - [git](https://git-scm.com/) 31 | - [VSCode](https://code.visualstudio.com/) (or [Cursor](https://cursor.com/)) 32 | - [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Docker Engine via [get.docker.com](https://get.docker.com/) (Linux Install Script) 33 | - [Python](https://www.python.org/downloads/) 34 | 35 | Open a command line (Terminal, VSCode Terminal, Cursor Terminal, Powershell, etc) 36 | 37 | Clone this Repo 38 | ```bash 39 | mkdir -p ~/dev/stock-trading-bot 40 | cd ~/dev/stock-trading-bot 41 | git clone https://github.com/codingforentrepreneurs/Stock-Trading-Bot . 42 | ``` 43 | 44 | Checkout the start branch 45 | ```bash 46 | git checkout start 47 | rm -rf .git 48 | git init 49 | git add --all 50 | git commit -m "It's my bot now" 51 | ``` 52 | 53 | Create a Python vitual environment 54 | _macOS/Linux/WSL_ 55 | ```bash 56 | python3.12 -m venv venv 57 | source venv/bin/activate 58 | ``` 59 | 60 | _windows powershell_ 61 | ```powershell 62 | c:\Path\To\Python312\python.exe -m venv venv 63 | .\venv\Scripts\activate 64 | ``` 65 | 66 | Install requirements 67 | ```bash 68 | (venv) python -m pip install -r requirements.txt 69 | ``` 70 | 71 | Docker Compose Up (for local TimescaleDB and Redis) 72 | ```bash 73 | docker compose -f compose.yaml up -d 74 | ``` 75 | > If you don't have Docker, use [TimescaleDB Cloud](tsdb.co/justin) and [Upstash Redis](https://upstash.com/?utm_source=cfe) 76 | 77 | Create `.env` in project root 78 | ```bash 79 | mkdir -p ~/dev/stock-trading-bot 80 | echo "" >> .env 81 | ``` 82 | 83 | Add `DATABASE_URL` and `REDIS_URL` to `.env` (these are based on the `compose.yaml` file): 84 | ```bash 85 | DATABASE_URL="postgresql://postgres:postgres@localhost:5431/postgres" 86 | REDIS_URL="redis://localhost:6378" 87 | ``` 88 | 89 | 90 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: timescale/timescaledb:latest-pg15 4 | ports: 5 | - "5431:5432" 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | - POSTGRES_DB=postgres 10 | volumes: 11 | - db-data:/var/lib/postgresql/data 12 | redis: 13 | image: redis:latest 14 | ports: 15 | - "6878:6379" 16 | volumes: 17 | - redis-data:/data 18 | 19 | volumes: 20 | db-data: 21 | redis-data: 22 | 23 | 24 | -------------------------------------------------------------------------------- /nbs/1 - Hello World Load dotenv.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "id": "f037d501-ba41-4a68-ba0c-26bb0d9c4ef3", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from decouple import config" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 5, 16 | "id": "db49840c-4176-4433-b706-b26016eb50e2", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "assert config(\"ALPHA_VANTAGE_API_KEY\", default=None, cast=str) is not None" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 6, 26 | "id": "e20f50e1-23d9-496c-a778-aeb8f2dd2957", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "assert config(\"POLOGYON_API_KEY\", default=None, cast=str) is not None" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "5863d4d8-12d4-44e9-a83d-eef2898fde36", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.12.3" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /nbs/10 - Load Stock Quotes into Django.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "da110c89-fc4d-4751-840b-2a95891c50bf", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import helpers.clients as helper_clients" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "15cec661-367b-45d0-a99d-919aaf786956", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "company_name = \"Apple\"\n", 32 | "company_ticker = \"AAPL\"\n", 33 | "multiplier = 1\n", 34 | "from_date = \"2023-01-09\"\n", 35 | "to_date = \"2023-01-09\"" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 4, 41 | "id": "ee0918d4-50a3-41f2-b00c-2c125ddeeeea", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "client = helper_clients.PolygonAPIClient(\n", 46 | " ticker=company_ticker,\n", 47 | " multiplier=multiplier,\n", 48 | " from_date=from_date,\n", 49 | " to_date=to_date\n", 50 | ")\n", 51 | "dataset = client.get_stock_data()" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 5, 57 | "id": "2de9b700-1002-480b-812d-6efd3c731ee8", 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "{'open_price': 129.6,\n", 64 | " 'close_price': 129.89,\n", 65 | " 'high_price': 130.3,\n", 66 | " 'low_price': 129.6,\n", 67 | " 'number_of_trades': 85,\n", 68 | " 'volume': 2312,\n", 69 | " 'volume_weighted_average': 129.8763,\n", 70 | " 'time': datetime.datetime(2023, 1, 9, 9, 0, tzinfo=)}" 71 | ] 72 | }, 73 | "execution_count": 5, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "new_stock_data = dataset[0]\n", 80 | "new_stock_data" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 6, 86 | "id": "b141e54a-9f1e-4538-b7d6-a4ad6d047066", 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "from market.models import Company, StockQuote" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 7, 96 | "id": "265e2daa-3a3a-4670-83eb-a0779a98ab72", 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "company_obj, created = Company.objects.get_or_create(name=company_name, ticker=company_ticker)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 8, 106 | "id": "71687c75-33fd-4ad6-8e5e-11f71251745d", 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/plain": [ 112 | "" 113 | ] 114 | }, 115 | "execution_count": 8, 116 | "metadata": {}, 117 | "output_type": "execute_result" 118 | } 119 | ], 120 | "source": [ 121 | "company_obj" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 10, 127 | "id": "c781ee86-155e-4cb7-ab3c-383a7496e3be", 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "ename": "IntegrityError", 132 | "evalue": "duplicate key value violates unique constraint \"1_2_market_stockquote_company_id_time_f15d280e_uniq\"\nDETAIL: Key (company_id, \"time\")=(1, 2023-01-09 09:00:00+00) already exists.", 133 | "output_type": "error", 134 | "traceback": [ 135 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 136 | "\u001b[0;31mUniqueViolation\u001b[0m Traceback (most recent call last)", 137 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:105\u001b[0m, in \u001b[0;36mCursorWrapper._execute\u001b[0;34m(self, sql, params, *ignored_wrapper_args)\u001b[0m\n\u001b[1;32m 104\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 105\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcursor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m)\u001b[49m\n", 138 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/psycopg/cursor.py:97\u001b[0m, in \u001b[0;36mCursor.execute\u001b[0;34m(self, query, params, prepare, binary)\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m e\u001b[38;5;241m.\u001b[39m_NO_TRACEBACK \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[0;32m---> 97\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ex\u001b[38;5;241m.\u001b[39mwith_traceback(\u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[1;32m 98\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", 139 | "\u001b[0;31mUniqueViolation\u001b[0m: duplicate key value violates unique constraint \"1_2_market_stockquote_company_id_time_f15d280e_uniq\"\nDETAIL: Key (company_id, \"time\")=(1, 2023-01-09 09:00:00+00) already exists.", 140 | "\nThe above exception was the direct cause of the following exception:\n", 141 | "\u001b[0;31mIntegrityError\u001b[0m Traceback (most recent call last)", 142 | "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mStockQuote\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobjects\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcompany\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcompany_obj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mnew_stock_data\u001b[49m\u001b[43m)\u001b[49m\n", 143 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/manager.py:87\u001b[0m, in \u001b[0;36mBaseManager._get_queryset_methods..create_method..manager_method\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[38;5;129m@wraps\u001b[39m(method)\n\u001b[1;32m 86\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mmanager_method\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 87\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mgetattr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_queryset\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", 144 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/query.py:679\u001b[0m, in \u001b[0;36mQuerySet.create\u001b[0;34m(self, **kwargs)\u001b[0m\n\u001b[1;32m 677\u001b[0m obj \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmodel(\u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 678\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_for_write \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m--> 679\u001b[0m \u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[43mforce_insert\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43musing\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdb\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 680\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m obj\n", 145 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/base.py:891\u001b[0m, in \u001b[0;36mModel.save\u001b[0;34m(self, force_insert, force_update, using, update_fields, *args)\u001b[0m\n\u001b[1;32m 888\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m loaded_fields:\n\u001b[1;32m 889\u001b[0m update_fields \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mfrozenset\u001b[39m(loaded_fields)\n\u001b[0;32m--> 891\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave_base\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 892\u001b[0m \u001b[43m \u001b[49m\u001b[43musing\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43musing\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 893\u001b[0m \u001b[43m \u001b[49m\u001b[43mforce_insert\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mforce_insert\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 894\u001b[0m \u001b[43m \u001b[49m\u001b[43mforce_update\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mforce_update\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 895\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdate_fields\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mupdate_fields\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 896\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", 146 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/base.py:997\u001b[0m, in \u001b[0;36mModel.save_base\u001b[0;34m(self, raw, force_insert, force_update, using, update_fields)\u001b[0m\n\u001b[1;32m 993\u001b[0m force_insert \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_validate_force_insert(force_insert)\n\u001b[1;32m 994\u001b[0m parent_inserted \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_save_parents(\n\u001b[1;32m 995\u001b[0m \u001b[38;5;28mcls\u001b[39m, using, update_fields, force_insert\n\u001b[1;32m 996\u001b[0m )\n\u001b[0;32m--> 997\u001b[0m updated \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_save_table\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 998\u001b[0m \u001b[43m \u001b[49m\u001b[43mraw\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 999\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1000\u001b[0m \u001b[43m \u001b[49m\u001b[43mforce_insert\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mparent_inserted\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1001\u001b[0m \u001b[43m \u001b[49m\u001b[43mforce_update\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[43m \u001b[49m\u001b[43musing\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1003\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdate_fields\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1004\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1005\u001b[0m \u001b[38;5;66;03m# Store the database on which the object was saved\u001b[39;00m\n\u001b[1;32m 1006\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_state\u001b[38;5;241m.\u001b[39mdb \u001b[38;5;241m=\u001b[39m using\n", 147 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/base.py:1160\u001b[0m, in \u001b[0;36mModel._save_table\u001b[0;34m(self, raw, cls, force_insert, force_update, using, update_fields)\u001b[0m\n\u001b[1;32m 1154\u001b[0m fields \u001b[38;5;241m=\u001b[39m [\n\u001b[1;32m 1155\u001b[0m f\n\u001b[1;32m 1156\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m meta\u001b[38;5;241m.\u001b[39mlocal_concrete_fields\n\u001b[1;32m 1157\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m f\u001b[38;5;241m.\u001b[39mgenerated \u001b[38;5;129;01mand\u001b[39;00m (pk_set \u001b[38;5;129;01mor\u001b[39;00m f \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m meta\u001b[38;5;241m.\u001b[39mauto_field)\n\u001b[1;32m 1158\u001b[0m ]\n\u001b[1;32m 1159\u001b[0m returning_fields \u001b[38;5;241m=\u001b[39m meta\u001b[38;5;241m.\u001b[39mdb_returning_fields\n\u001b[0;32m-> 1160\u001b[0m results \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_do_insert\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1161\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mcls\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_base_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43musing\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfields\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreturning_fields\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mraw\u001b[49m\n\u001b[1;32m 1162\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1163\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m results:\n\u001b[1;32m 1164\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m value, field \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(results[\u001b[38;5;241m0\u001b[39m], returning_fields):\n", 148 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/base.py:1201\u001b[0m, in \u001b[0;36mModel._do_insert\u001b[0;34m(self, manager, using, fields, returning_fields, raw)\u001b[0m\n\u001b[1;32m 1196\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_do_insert\u001b[39m(\u001b[38;5;28mself\u001b[39m, manager, using, fields, returning_fields, raw):\n\u001b[1;32m 1197\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1198\u001b[0m \u001b[38;5;124;03m Do an INSERT. If returning_fields is defined then this method should\u001b[39;00m\n\u001b[1;32m 1199\u001b[0m \u001b[38;5;124;03m return the newly created data for the model.\u001b[39;00m\n\u001b[1;32m 1200\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1201\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mmanager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_insert\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1202\u001b[0m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1203\u001b[0m \u001b[43m \u001b[49m\u001b[43mfields\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfields\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1204\u001b[0m \u001b[43m \u001b[49m\u001b[43mreturning_fields\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mreturning_fields\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1205\u001b[0m \u001b[43m \u001b[49m\u001b[43musing\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43musing\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1206\u001b[0m \u001b[43m \u001b[49m\u001b[43mraw\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mraw\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1207\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", 149 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/manager.py:87\u001b[0m, in \u001b[0;36mBaseManager._get_queryset_methods..create_method..manager_method\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[38;5;129m@wraps\u001b[39m(method)\n\u001b[1;32m 86\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mmanager_method\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 87\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mgetattr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_queryset\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", 150 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/query.py:1847\u001b[0m, in \u001b[0;36mQuerySet._insert\u001b[0;34m(self, objs, fields, returning_fields, raw, using, on_conflict, update_fields, unique_fields)\u001b[0m\n\u001b[1;32m 1840\u001b[0m query \u001b[38;5;241m=\u001b[39m sql\u001b[38;5;241m.\u001b[39mInsertQuery(\n\u001b[1;32m 1841\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmodel,\n\u001b[1;32m 1842\u001b[0m on_conflict\u001b[38;5;241m=\u001b[39mon_conflict,\n\u001b[1;32m 1843\u001b[0m update_fields\u001b[38;5;241m=\u001b[39mupdate_fields,\n\u001b[1;32m 1844\u001b[0m unique_fields\u001b[38;5;241m=\u001b[39munique_fields,\n\u001b[1;32m 1845\u001b[0m )\n\u001b[1;32m 1846\u001b[0m query\u001b[38;5;241m.\u001b[39minsert_values(fields, objs, raw\u001b[38;5;241m=\u001b[39mraw)\n\u001b[0;32m-> 1847\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mquery\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_compiler\u001b[49m\u001b[43m(\u001b[49m\u001b[43musing\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43musing\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_sql\u001b[49m\u001b[43m(\u001b[49m\u001b[43mreturning_fields\u001b[49m\u001b[43m)\u001b[49m\n", 151 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py:1836\u001b[0m, in \u001b[0;36mSQLInsertCompiler.execute_sql\u001b[0;34m(self, returning_fields)\u001b[0m\n\u001b[1;32m 1834\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconnection\u001b[38;5;241m.\u001b[39mcursor() \u001b[38;5;28;01mas\u001b[39;00m cursor:\n\u001b[1;32m 1835\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m sql, params \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mas_sql():\n\u001b[0;32m-> 1836\u001b[0m \u001b[43mcursor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1837\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mreturning_fields:\n\u001b[1;32m 1838\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m []\n", 152 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:122\u001b[0m, in \u001b[0;36mCursorDebugWrapper.execute\u001b[0;34m(self, sql, params)\u001b[0m\n\u001b[1;32m 120\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mexecute\u001b[39m(\u001b[38;5;28mself\u001b[39m, sql, params\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 121\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdebug_sql(sql, params, use_last_executed_query\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[0;32m--> 122\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m)\u001b[49m\n", 153 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:79\u001b[0m, in \u001b[0;36mCursorWrapper.execute\u001b[0;34m(self, sql, params)\u001b[0m\n\u001b[1;32m 78\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mexecute\u001b[39m(\u001b[38;5;28mself\u001b[39m, sql, params\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[0;32m---> 79\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_execute_with_wrappers\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 80\u001b[0m \u001b[43m \u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmany\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecutor\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_execute\u001b[49m\n\u001b[1;32m 81\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", 154 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:92\u001b[0m, in \u001b[0;36mCursorWrapper._execute_with_wrappers\u001b[0;34m(self, sql, params, many, executor)\u001b[0m\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m wrapper \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mreversed\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdb\u001b[38;5;241m.\u001b[39mexecute_wrappers):\n\u001b[1;32m 91\u001b[0m executor \u001b[38;5;241m=\u001b[39m functools\u001b[38;5;241m.\u001b[39mpartial(wrapper, executor)\n\u001b[0;32m---> 92\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mexecutor\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmany\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[43m)\u001b[49m\n", 155 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:100\u001b[0m, in \u001b[0;36mCursorWrapper._execute\u001b[0;34m(self, sql, params, *ignored_wrapper_args)\u001b[0m\n\u001b[1;32m 98\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mAPPS_NOT_READY_WARNING_MSG, category\u001b[38;5;241m=\u001b[39m\u001b[38;5;167;01mRuntimeWarning\u001b[39;00m)\n\u001b[1;32m 99\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdb\u001b[38;5;241m.\u001b[39mvalidate_no_broken_transaction()\n\u001b[0;32m--> 100\u001b[0m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mwith\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdb\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwrap_database_errors\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 101\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mis\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m:\u001b[49m\n\u001b[1;32m 102\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# params default might be backend specific.\u001b[39;49;00m\n\u001b[1;32m 103\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mreturn\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcursor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m)\u001b[49m\n", 156 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/utils.py:91\u001b[0m, in \u001b[0;36mDatabaseErrorWrapper.__exit__\u001b[0;34m(self, exc_type, exc_value, traceback)\u001b[0m\n\u001b[1;32m 89\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m dj_exc_type \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;129;01min\u001b[39;00m (DataError, IntegrityError):\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mwrapper\u001b[38;5;241m.\u001b[39merrors_occurred \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m---> 91\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m dj_exc_value\u001b[38;5;241m.\u001b[39mwith_traceback(traceback) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mexc_value\u001b[39;00m\n", 157 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/django/db/backends/utils.py:105\u001b[0m, in \u001b[0;36mCursorWrapper._execute\u001b[0;34m(self, sql, params, *ignored_wrapper_args)\u001b[0m\n\u001b[1;32m 103\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcursor\u001b[38;5;241m.\u001b[39mexecute(sql)\n\u001b[1;32m 104\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 105\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcursor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mparams\u001b[49m\u001b[43m)\u001b[49m\n", 158 | "File \u001b[0;32m~/Dev/stock-trading-bot/venv/lib/python3.12/site-packages/psycopg/cursor.py:97\u001b[0m, in \u001b[0;36mCursor.execute\u001b[0;34m(self, query, params, prepare, binary)\u001b[0m\n\u001b[1;32m 93\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_conn\u001b[38;5;241m.\u001b[39mwait(\n\u001b[1;32m 94\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_execute_gen(query, params, prepare\u001b[38;5;241m=\u001b[39mprepare, binary\u001b[38;5;241m=\u001b[39mbinary)\n\u001b[1;32m 95\u001b[0m )\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m e\u001b[38;5;241m.\u001b[39m_NO_TRACEBACK \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[0;32m---> 97\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ex\u001b[38;5;241m.\u001b[39mwith_traceback(\u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[1;32m 98\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", 159 | "\u001b[0;31mIntegrityError\u001b[0m: duplicate key value violates unique constraint \"1_2_market_stockquote_company_id_time_f15d280e_uniq\"\nDETAIL: Key (company_id, \"time\")=(1, 2023-01-09 09:00:00+00) already exists." 160 | ] 161 | } 162 | ], 163 | "source": [ 164 | "StockQuote.objects.create(company=company_obj, **new_stock_data)" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "id": "058609c2-8f04-492b-9121-4ac8ecded050", 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [] 174 | } 175 | ], 176 | "metadata": { 177 | "kernelspec": { 178 | "display_name": "Python 3 (ipykernel)", 179 | "language": "python", 180 | "name": "python3" 181 | }, 182 | "language_info": { 183 | "codemirror_mode": { 184 | "name": "ipython", 185 | "version": 3 186 | }, 187 | "file_extension": ".py", 188 | "mimetype": "text/x-python", 189 | "name": "python", 190 | "nbconvert_exporter": "python", 191 | "pygments_lexer": "ipython3", 192 | "version": "3.12.3" 193 | } 194 | }, 195 | "nbformat": 4, 196 | "nbformat_minor": 5 197 | } 198 | -------------------------------------------------------------------------------- /nbs/11 - Batch and Bulk Load Stock Quotes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 8, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 9, 17 | "id": "da110c89-fc4d-4751-840b-2a95891c50bf", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import helpers.clients as helper_clients" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 38, 27 | "id": "15cec661-367b-45d0-a99d-919aaf786956", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "company_name = \"Google\"\n", 32 | "company_ticker = \"GOOG\"\n", 33 | "multiplier = 1\n", 34 | "from_date = \"2023-01-09\"\n", 35 | "to_date = \"2023-04-09\"" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 39, 41 | "id": "ee0918d4-50a3-41f2-b00c-2c125ddeeeea", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "client = helper_clients.PolygonAPIClient(\n", 46 | " ticker=company_ticker,\n", 47 | " multiplier=multiplier,\n", 48 | " from_date=from_date,\n", 49 | " to_date=to_date\n", 50 | ")\n", 51 | "dataset = client.get_stock_data()" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 40, 57 | "id": "f3a86d92-c0ad-46ab-a59a-1f095ff6f316", 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "5000" 64 | ] 65 | }, 66 | "execution_count": 40, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "len(dataset)" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 41, 78 | "id": "2de9b700-1002-480b-812d-6efd3c731ee8", 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "data": { 83 | "text/plain": [ 84 | "{'open_price': 88.08,\n", 85 | " 'close_price': 88.16,\n", 86 | " 'high_price': 88.17,\n", 87 | " 'low_price': 88.08,\n", 88 | " 'number_of_trades': 55,\n", 89 | " 'volume': 1684,\n", 90 | " 'volume_weighted_average': 88.1217,\n", 91 | " 'time': datetime.datetime(2023, 1, 9, 9, 0, tzinfo=)}" 92 | ] 93 | }, 94 | "execution_count": 41, 95 | "metadata": {}, 96 | "output_type": "execute_result" 97 | } 98 | ], 99 | "source": [ 100 | "new_stock_data = dataset[0]\n", 101 | "new_stock_data" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 42, 107 | "id": "b141e54a-9f1e-4538-b7d6-a4ad6d047066", 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "from market.models import Company, StockQuote" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 43, 117 | "id": "265e2daa-3a3a-4670-83eb-a0779a98ab72", 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "company_obj, created = Company.objects.get_or_create(name=company_name, ticker=company_ticker)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 44, 127 | "id": "71687c75-33fd-4ad6-8e5e-11f71251745d", 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "data": { 132 | "text/plain": [ 133 | "" 134 | ] 135 | }, 136 | "execution_count": 44, 137 | "metadata": {}, 138 | "output_type": "execute_result" 139 | } 140 | ], 141 | "source": [ 142 | "company_obj" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 45, 148 | "id": "c781ee86-155e-4cb7-ab3c-383a7496e3be", 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "# StockQuote.objects.bulk_create(company=company_obj, **new_stock_data)" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 46, 158 | "id": "0716fedc-726a-4f42-81f3-45c341f0b65e", 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "# new_quotes = []\n", 163 | "\n", 164 | "# for data in dataset:\n", 165 | "# new_quotes.append(\n", 166 | "# StockQuote(company=company_obj, **data)\n", 167 | "# )" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "id": "21749118-44e8-4b40-ae24-0bc48c056019", 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 47, 181 | "id": "ee625d77-7adb-43db-a33c-1a9cfade4e2f", 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "data": { 186 | "text/plain": [ 187 | ", , , , , , , , , , , , , , , , , , , , '...(remaining elements truncated)...']>" 188 | ] 189 | }, 190 | "execution_count": 47, 191 | "metadata": {}, 192 | "output_type": "execute_result" 193 | } 194 | ], 195 | "source": [ 196 | "StockQuote.objects.all()" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 48, 202 | "id": "a79a6b44-2ae0-41d2-ace4-2e9e4075754c", 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "batch_size = 1000\n", 207 | "for i in range(0, len(dataset), batch_size):\n", 208 | " batch_chunk = dataset[i:i+batch_size]\n", 209 | " chunked_quotes = []\n", 210 | " for data in batch_chunk:\n", 211 | " chunked_quotes.append(\n", 212 | " StockQuote(company=company_obj, **data)\n", 213 | " )\n", 214 | " StockQuote.objects.bulk_create(chunked_quotes, ignore_conflicts=True)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 49, 220 | "id": "9e151fb7-4adc-4f0a-9108-37253ede501a", 221 | "metadata": {}, 222 | "outputs": [ 223 | { 224 | "data": { 225 | "text/plain": [ 226 | "10000" 227 | ] 228 | }, 229 | "execution_count": 49, 230 | "metadata": {}, 231 | "output_type": "execute_result" 232 | } 233 | ], 234 | "source": [ 235 | "StockQuote.objects.all().count()" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": null, 241 | "id": "6e82b7b1-6440-476a-939d-2fb41da5bad4", 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [] 245 | } 246 | ], 247 | "metadata": { 248 | "kernelspec": { 249 | "display_name": "Python 3 (ipykernel)", 250 | "language": "python", 251 | "name": "python3" 252 | }, 253 | "language_info": { 254 | "codemirror_mode": { 255 | "name": "ipython", 256 | "version": 3 257 | }, 258 | "file_extension": ".py", 259 | "mimetype": "text/x-python", 260 | "name": "python", 261 | "nbconvert_exporter": "python", 262 | "pygments_lexer": "ipython3", 263 | "version": "3.12.3" 264 | } 265 | }, 266 | "nbformat": 4, 267 | "nbformat_minor": 5 268 | } 269 | -------------------------------------------------------------------------------- /nbs/12 - Stock Sync.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "b5f1177d-e522-49bc-b1c5-3a789fc5f705", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market.models import *" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "f3298897-bf95-471b-bae6-37cacb7ed81c", 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "data": { 32 | "text/plain": [ 33 | "4" 34 | ] 35 | }, 36 | "execution_count": 3, 37 | "metadata": {}, 38 | "output_type": "execute_result" 39 | } 40 | ], 41 | "source": [ 42 | "qs = Company.objects.filter(active=True)\n", 43 | "qs.count()" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 4, 49 | "id": "f30c0322-e35f-4e18-9a2c-50d268e5aed8", 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "obj, created = Company.objects.get_or_create(name='Meta', ticker='META')\n", 54 | "obj.save()\n", 55 | "# force ticker to be uppercase" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 5, 61 | "id": "f8c28536-d16e-41c2-8284-46577078612e", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "for company in qs:\n", 66 | " company.save()" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 6, 72 | "id": "4c5bf189-0375-4420-9c4d-b5315dd0aa9b", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "# Company.objects.create(name='Microsoft', ticker='MSFT')" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 7, 82 | "id": "86fd4a90-c350-4688-8f81-a69826dff6ec", 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "data": { 87 | "text/plain": [ 88 | "701190" 89 | ] 90 | }, 91 | "execution_count": 7, 92 | "metadata": {}, 93 | "output_type": "execute_result" 94 | } 95 | ], 96 | "source": [ 97 | "stocks = StockQuote.objects.all()\n", 98 | "stocks.count()" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 8, 104 | "id": "5126a4b3-b93c-4094-8bd3-b53c949efa16", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "start_from_strach = False\n", 109 | "if start_from_strach:\n", 110 | " stocks.delete()" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 9, 116 | "id": "08e405d1-d119-4710-bf34-ca66c381bb82", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "from market import tasks as market_tasks" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 10, 126 | "id": "a7cb0d48-b7f2-41de-acc0-e457c677e47f", 127 | "metadata": {}, 128 | "outputs": [ 129 | { 130 | "data": { 131 | "text/plain": [ 132 | "" 133 | ] 134 | }, 135 | "execution_count": 10, 136 | "metadata": {}, 137 | "output_type": "execute_result" 138 | } 139 | ], 140 | "source": [ 141 | "market_tasks.sync_stock_data.delay()" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 15, 147 | "id": "6dae3ec6-9d74-4680-a1e0-c8100291b012", 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "data": { 152 | "text/plain": [ 153 | "1295" 154 | ] 155 | }, 156 | "execution_count": 15, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "StockQuote.objects.all().count()" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "e25e1d09-d4a0-4018-81b4-6bc33310c82d", 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "id": "33bdaa0b-88d7-445a-bcbe-0296c416a0ea", 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 3 (ipykernel)", 185 | "language": "python", 186 | "name": "python3" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 3 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython3", 198 | "version": "3.12.3" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 5 203 | } 204 | -------------------------------------------------------------------------------- /nbs/3 - Hello World Alpha Vantage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 6 | "id": "f037d501-ba41-4a68-ba0c-26bb0d9c4ef3", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from decouple import config" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 4, 16 | "id": "db49840c-4176-4433-b706-b26016eb50e2", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "assert config(\"ALPHA_VANTAGE_API_KEY\", default=None, cast=str) is not None" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 5, 26 | "id": "e20f50e1-23d9-496c-a778-aeb8f2dd2957", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "ALPHA_VANTAGE_API_KEY = config(\"ALPHA_VANTAGE_API_KEY\", default=None, cast=str)" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 6, 36 | "id": "5863d4d8-12d4-44e9-a83d-eef2898fde36", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "import requests\n", 41 | "\n", 42 | "# replace the \"demo\" apikey below with your own key from https://www.alphavantage.co/support/#api-key\n", 43 | "\n", 44 | "\n", 45 | "api_key = ALPHA_VANTAGE_API_KEY\n", 46 | "\n", 47 | "params = {\n", 48 | " \"api_key\": ALPHA_VANTAGE_API_KEY,\n", 49 | " \"ticker\": \"AAPL\",\n", 50 | " \"function\": \"TIME_SERIES_INTRADAY\"\n", 51 | "}\n", 52 | "\n", 53 | "url = 'https://www.alphavantage.co/query?function={function}&symbol={ticker}&interval=5min&apikey={api_key}'.format(\n", 54 | " **params\n", 55 | ")\n", 56 | "r = requests.get(url)\n", 57 | "data = r.json()" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 7, 63 | "id": "1d54d3df-1775-49d8-8bad-a6233dad5082", 64 | "metadata": {}, 65 | "outputs": [ 66 | { 67 | "data": { 68 | "text/plain": [ 69 | "{'Meta Data': {'1. Information': 'Intraday (5min) open, high, low, close prices and volume',\n", 70 | " '2. Symbol': 'AAPL',\n", 71 | " '3. Last Refreshed': '2024-11-05 19:55:00',\n", 72 | " '4. Interval': '5min',\n", 73 | " '5. Output Size': 'Compact',\n", 74 | " '6. Time Zone': 'US/Eastern'},\n", 75 | " 'Time Series (5min)': {'2024-11-05 19:55:00': {'1. open': '222.8500',\n", 76 | " '2. high': '223.1500',\n", 77 | " '3. low': '222.8000',\n", 78 | " '4. close': '223.0000',\n", 79 | " '5. volume': '9097'},\n", 80 | " '2024-11-05 19:50:00': {'1. open': '222.5400',\n", 81 | " '2. high': '222.8800',\n", 82 | " '3. low': '222.5400',\n", 83 | " '4. close': '222.8400',\n", 84 | " '5. volume': '2610'},\n", 85 | " '2024-11-05 19:45:00': {'1. open': '222.8800',\n", 86 | " '2. high': '222.9300',\n", 87 | " '3. low': '222.5100',\n", 88 | " '4. close': '222.6800',\n", 89 | " '5. volume': '2050'},\n", 90 | " '2024-11-05 19:40:00': {'1. open': '222.4700',\n", 91 | " '2. high': '223.1000',\n", 92 | " '3. low': '222.0100',\n", 93 | " '4. close': '222.8500',\n", 94 | " '5. volume': '2805'},\n", 95 | " '2024-11-05 19:35:00': {'1. open': '222.9200',\n", 96 | " '2. high': '223.2000',\n", 97 | " '3. low': '222.4500',\n", 98 | " '4. close': '222.4800',\n", 99 | " '5. volume': '2706'},\n", 100 | " '2024-11-05 19:30:00': {'1. open': '222.3500',\n", 101 | " '2. high': '223.2000',\n", 102 | " '3. low': '222.2300',\n", 103 | " '4. close': '222.9800',\n", 104 | " '5. volume': '7919'},\n", 105 | " '2024-11-05 19:25:00': {'1. open': '222.9000',\n", 106 | " '2. high': '223.2700',\n", 107 | " '3. low': '222.3500',\n", 108 | " '4. close': '222.4000',\n", 109 | " '5. volume': '13396'},\n", 110 | " '2024-11-05 19:20:00': {'1. open': '222.9600',\n", 111 | " '2. high': '223.2600',\n", 112 | " '3. low': '222.9000',\n", 113 | " '4. close': '222.9000',\n", 114 | " '5. volume': '3345'},\n", 115 | " '2024-11-05 19:15:00': {'1. open': '222.8300',\n", 116 | " '2. high': '222.9888',\n", 117 | " '3. low': '222.6000',\n", 118 | " '4. close': '222.9450',\n", 119 | " '5. volume': '2641'},\n", 120 | " '2024-11-05 19:10:00': {'1. open': '222.8950',\n", 121 | " '2. high': '222.9800',\n", 122 | " '3. low': '222.7000',\n", 123 | " '4. close': '222.9500',\n", 124 | " '5. volume': '3153'},\n", 125 | " '2024-11-05 19:05:00': {'1. open': '222.9700',\n", 126 | " '2. high': '222.9800',\n", 127 | " '3. low': '222.8100',\n", 128 | " '4. close': '222.8900',\n", 129 | " '5. volume': '2988'},\n", 130 | " '2024-11-05 19:00:00': {'1. open': '222.9800',\n", 131 | " '2. high': '223.0000',\n", 132 | " '3. low': '222.9600',\n", 133 | " '4. close': '222.9700',\n", 134 | " '5. volume': '708'},\n", 135 | " '2024-11-05 18:55:00': {'1. open': '222.9600',\n", 136 | " '2. high': '223.0000',\n", 137 | " '3. low': '222.9600',\n", 138 | " '4. close': '222.9700',\n", 139 | " '5. volume': '605'},\n", 140 | " '2024-11-05 18:50:00': {'1. open': '223.0450',\n", 141 | " '2. high': '223.0900',\n", 142 | " '3. low': '222.9600',\n", 143 | " '4. close': '222.9600',\n", 144 | " '5. volume': '2446'},\n", 145 | " '2024-11-05 18:45:00': {'1. open': '223.0650',\n", 146 | " '2. high': '223.0900',\n", 147 | " '3. low': '223.0000',\n", 148 | " '4. close': '223.0450',\n", 149 | " '5. volume': '167'},\n", 150 | " '2024-11-05 18:40:00': {'1. open': '223.0998',\n", 151 | " '2. high': '223.1000',\n", 152 | " '3. low': '223.0400',\n", 153 | " '4. close': '223.0650',\n", 154 | " '5. volume': '714'},\n", 155 | " '2024-11-05 18:35:00': {'1. open': '223.1200',\n", 156 | " '2. high': '223.1200',\n", 157 | " '3. low': '223.0400',\n", 158 | " '4. close': '223.0400',\n", 159 | " '5. volume': '1198'},\n", 160 | " '2024-11-05 18:30:00': {'1. open': '223.0300',\n", 161 | " '2. high': '223.1200',\n", 162 | " '3. low': '223.0300',\n", 163 | " '4. close': '223.0420',\n", 164 | " '5. volume': '4922'},\n", 165 | " '2024-11-05 18:25:00': {'1. open': '223.0600',\n", 166 | " '2. high': '223.0700',\n", 167 | " '3. low': '223.0000',\n", 168 | " '4. close': '223.0300',\n", 169 | " '5. volume': '1022'},\n", 170 | " '2024-11-05 18:20:00': {'1. open': '223.0200',\n", 171 | " '2. high': '223.1200',\n", 172 | " '3. low': '222.9200',\n", 173 | " '4. close': '223.0700',\n", 174 | " '5. volume': '659'},\n", 175 | " '2024-11-05 18:15:00': {'1. open': '223.0900',\n", 176 | " '2. high': '223.1200',\n", 177 | " '3. low': '222.9100',\n", 178 | " '4. close': '223.0200',\n", 179 | " '5. volume': '1545'},\n", 180 | " '2024-11-05 18:10:00': {'1. open': '223.1100',\n", 181 | " '2. high': '223.1200',\n", 182 | " '3. low': '222.9100',\n", 183 | " '4. close': '223.0150',\n", 184 | " '5. volume': '889'},\n", 185 | " '2024-11-05 18:05:00': {'1. open': '222.8700',\n", 186 | " '2. high': '223.1100',\n", 187 | " '3. low': '222.8700',\n", 188 | " '4. close': '223.0150',\n", 189 | " '5. volume': '1783'},\n", 190 | " '2024-11-05 18:00:00': {'1. open': '223.0650',\n", 191 | " '2. high': '223.1200',\n", 192 | " '3. low': '222.8700',\n", 193 | " '4. close': '222.9900',\n", 194 | " '5. volume': '7505'},\n", 195 | " '2024-11-05 17:55:00': {'1. open': '223.1200',\n", 196 | " '2. high': '223.1200',\n", 197 | " '3. low': '223.0200',\n", 198 | " '4. close': '223.0700',\n", 199 | " '5. volume': '2157'},\n", 200 | " '2024-11-05 17:50:00': {'1. open': '223.0750',\n", 201 | " '2. high': '223.1200',\n", 202 | " '3. low': '223.0200',\n", 203 | " '4. close': '223.0400',\n", 204 | " '5. volume': '782'},\n", 205 | " '2024-11-05 17:45:00': {'1. open': '223.0800',\n", 206 | " '2. high': '223.1200',\n", 207 | " '3. low': '223.0200',\n", 208 | " '4. close': '223.1000',\n", 209 | " '5. volume': '1040'},\n", 210 | " '2024-11-05 17:40:00': {'1. open': '223.0950',\n", 211 | " '2. high': '223.1400',\n", 212 | " '3. low': '223.0100',\n", 213 | " '4. close': '223.0500',\n", 214 | " '5. volume': '1785'},\n", 215 | " '2024-11-05 17:35:00': {'1. open': '223.1000',\n", 216 | " '2. high': '223.4500',\n", 217 | " '3. low': '223.0500',\n", 218 | " '4. close': '223.0500',\n", 219 | " '5. volume': '98441'},\n", 220 | " '2024-11-05 17:30:00': {'1. open': '223.1500',\n", 221 | " '2. high': '223.2600',\n", 222 | " '3. low': '223.0200',\n", 223 | " '4. close': '223.1000',\n", 224 | " '5. volume': '4610'},\n", 225 | " '2024-11-05 17:25:00': {'1. open': '223.1600',\n", 226 | " '2. high': '223.2800',\n", 227 | " '3. low': '223.0500',\n", 228 | " '4. close': '223.1500',\n", 229 | " '5. volume': '845'},\n", 230 | " '2024-11-05 17:20:00': {'1. open': '223.1600',\n", 231 | " '2. high': '223.1750',\n", 232 | " '3. low': '223.0500',\n", 233 | " '4. close': '223.1750',\n", 234 | " '5. volume': '2303'},\n", 235 | " '2024-11-05 17:15:00': {'1. open': '223.2200',\n", 236 | " '2. high': '228.3750',\n", 237 | " '3. low': '223.0500',\n", 238 | " '4. close': '223.1400',\n", 239 | " '5. volume': '646'},\n", 240 | " '2024-11-05 17:10:00': {'1. open': '223.2900',\n", 241 | " '2. high': '223.3000',\n", 242 | " '3. low': '223.2000',\n", 243 | " '4. close': '223.2300',\n", 244 | " '5. volume': '1189'},\n", 245 | " '2024-11-05 17:05:00': {'1. open': '223.2450',\n", 246 | " '2. high': '223.4500',\n", 247 | " '3. low': '223.2000',\n", 248 | " '4. close': '223.2900',\n", 249 | " '5. volume': '27501'},\n", 250 | " '2024-11-05 17:00:00': {'1. open': '223.2300',\n", 251 | " '2. high': '223.2900',\n", 252 | " '3. low': '210.5870',\n", 253 | " '4. close': '223.2450',\n", 254 | " '5. volume': '2642'},\n", 255 | " '2024-11-05 16:55:00': {'1. open': '223.0400',\n", 256 | " '2. high': '223.2900',\n", 257 | " '3. low': '210.0654',\n", 258 | " '4. close': '223.2800',\n", 259 | " '5. volume': '8849'},\n", 260 | " '2024-11-05 16:50:00': {'1. open': '223.0300',\n", 261 | " '2. high': '223.1600',\n", 262 | " '3. low': '210.9498',\n", 263 | " '4. close': '210.9498',\n", 264 | " '5. volume': '4809'},\n", 265 | " '2024-11-05 16:45:00': {'1. open': '223.1500',\n", 266 | " '2. high': '223.3200',\n", 267 | " '3. low': '219.5283',\n", 268 | " '4. close': '223.0113',\n", 269 | " '5. volume': '7289'},\n", 270 | " '2024-11-05 16:40:00': {'1. open': '223.2600',\n", 271 | " '2. high': '223.4500',\n", 272 | " '3. low': '223.1400',\n", 273 | " '4. close': '223.1600',\n", 274 | " '5. volume': '6505'},\n", 275 | " '2024-11-05 16:35:00': {'1. open': '223.2200',\n", 276 | " '2. high': '223.4500',\n", 277 | " '3. low': '223.2100',\n", 278 | " '4. close': '223.2550',\n", 279 | " '5. volume': '5498'},\n", 280 | " '2024-11-05 16:30:00': {'1. open': '223.3000',\n", 281 | " '2. high': '223.3400',\n", 282 | " '3. low': '223.2000',\n", 283 | " '4. close': '223.2200',\n", 284 | " '5. volume': '6412'},\n", 285 | " '2024-11-05 16:25:00': {'1. open': '223.4100',\n", 286 | " '2. high': '223.4442',\n", 287 | " '3. low': '223.2200',\n", 288 | " '4. close': '223.3400',\n", 289 | " '5. volume': '2330'},\n", 290 | " '2024-11-05 16:20:00': {'1. open': '223.2850',\n", 291 | " '2. high': '223.4200',\n", 292 | " '3. low': '223.2000',\n", 293 | " '4. close': '223.4200',\n", 294 | " '5. volume': '11763'},\n", 295 | " '2024-11-05 16:15:00': {'1. open': '223.3100',\n", 296 | " '2. high': '223.4500',\n", 297 | " '3. low': '223.2000',\n", 298 | " '4. close': '223.2000',\n", 299 | " '5. volume': '6082'},\n", 300 | " '2024-11-05 16:10:00': {'1. open': '223.4300',\n", 301 | " '2. high': '223.4500',\n", 302 | " '3. low': '223.2500',\n", 303 | " '4. close': '223.2750',\n", 304 | " '5. volume': '12641'},\n", 305 | " '2024-11-05 16:05:00': {'1. open': '223.4000',\n", 306 | " '2. high': '223.5900',\n", 307 | " '3. low': '223.3400',\n", 308 | " '4. close': '223.4300',\n", 309 | " '5. volume': '32836'},\n", 310 | " '2024-11-05 16:00:00': {'1. open': '223.4500',\n", 311 | " '2. high': '223.5900',\n", 312 | " '3. low': '223.2500',\n", 313 | " '4. close': '223.3800',\n", 314 | " '5. volume': '10148168'},\n", 315 | " '2024-11-05 15:55:00': {'1. open': '222.9600',\n", 316 | " '2. high': '223.5300',\n", 317 | " '3. low': '222.8700',\n", 318 | " '4. close': '223.4600',\n", 319 | " '5. volume': '1390327'},\n", 320 | " '2024-11-05 15:50:00': {'1. open': '222.8600',\n", 321 | " '2. high': '223.1200',\n", 322 | " '3. low': '222.7750',\n", 323 | " '4. close': '222.9600',\n", 324 | " '5. volume': '493692'},\n", 325 | " '2024-11-05 15:45:00': {'1. open': '222.8300',\n", 326 | " '2. high': '222.9200',\n", 327 | " '3. low': '222.7100',\n", 328 | " '4. close': '222.8650',\n", 329 | " '5. volume': '308174'},\n", 330 | " '2024-11-05 15:40:00': {'1. open': '223.3200',\n", 331 | " '2. high': '223.3620',\n", 332 | " '3. low': '222.8250',\n", 333 | " '4. close': '222.8300',\n", 334 | " '5. volume': '475711'},\n", 335 | " '2024-11-05 15:35:00': {'1. open': '223.4600',\n", 336 | " '2. high': '223.5000',\n", 337 | " '3. low': '223.3000',\n", 338 | " '4. close': '223.3300',\n", 339 | " '5. volume': '227018'},\n", 340 | " '2024-11-05 15:30:00': {'1. open': '223.2500',\n", 341 | " '2. high': '223.5300',\n", 342 | " '3. low': '223.1600',\n", 343 | " '4. close': '223.4600',\n", 344 | " '5. volume': '313863'},\n", 345 | " '2024-11-05 15:25:00': {'1. open': '223.1800',\n", 346 | " '2. high': '223.2600',\n", 347 | " '3. low': '223.0700',\n", 348 | " '4. close': '223.2500',\n", 349 | " '5. volume': '188701'},\n", 350 | " '2024-11-05 15:20:00': {'1. open': '223.2100',\n", 351 | " '2. high': '223.2400',\n", 352 | " '3. low': '223.1200',\n", 353 | " '4. close': '223.1700',\n", 354 | " '5. volume': '165666'},\n", 355 | " '2024-11-05 15:15:00': {'1. open': '223.2050',\n", 356 | " '2. high': '223.2498',\n", 357 | " '3. low': '223.1500',\n", 358 | " '4. close': '223.2100',\n", 359 | " '5. volume': '137815'},\n", 360 | " '2024-11-05 15:10:00': {'1. open': '223.2008',\n", 361 | " '2. high': '223.2900',\n", 362 | " '3. low': '223.1676',\n", 363 | " '4. close': '223.2200',\n", 364 | " '5. volume': '163853'},\n", 365 | " '2024-11-05 15:05:00': {'1. open': '222.9400',\n", 366 | " '2. high': '223.2899',\n", 367 | " '3. low': '222.9400',\n", 368 | " '4. close': '223.2000',\n", 369 | " '5. volume': '180876'},\n", 370 | " '2024-11-05 15:00:00': {'1. open': '222.9300',\n", 371 | " '2. high': '223.0500',\n", 372 | " '3. low': '222.8201',\n", 373 | " '4. close': '222.9350',\n", 374 | " '5. volume': '146889'},\n", 375 | " '2024-11-05 14:55:00': {'1. open': '222.9450',\n", 376 | " '2. high': '223.0400',\n", 377 | " '3. low': '222.8100',\n", 378 | " '4. close': '222.9399',\n", 379 | " '5. volume': '202626'},\n", 380 | " '2024-11-05 14:50:00': {'1. open': '223.2500',\n", 381 | " '2. high': '223.2500',\n", 382 | " '3. low': '222.9300',\n", 383 | " '4. close': '222.9400',\n", 384 | " '5. volume': '201555'},\n", 385 | " '2024-11-05 14:45:00': {'1. open': '223.2550',\n", 386 | " '2. high': '223.2605',\n", 387 | " '3. low': '223.0999',\n", 388 | " '4. close': '223.2475',\n", 389 | " '5. volume': '140700'},\n", 390 | " '2024-11-05 14:40:00': {'1. open': '223.2700',\n", 391 | " '2. high': '223.3400',\n", 392 | " '3. low': '223.2400',\n", 393 | " '4. close': '223.2556',\n", 394 | " '5. volume': '123099'},\n", 395 | " '2024-11-05 14:35:00': {'1. open': '223.3550',\n", 396 | " '2. high': '223.3751',\n", 397 | " '3. low': '223.2500',\n", 398 | " '4. close': '223.2600',\n", 399 | " '5. volume': '128228'},\n", 400 | " '2024-11-05 14:30:00': {'1. open': '223.3400',\n", 401 | " '2. high': '223.4471',\n", 402 | " '3. low': '223.2600',\n", 403 | " '4. close': '223.3545',\n", 404 | " '5. volume': '159571'},\n", 405 | " '2024-11-05 14:25:00': {'1. open': '222.9100',\n", 406 | " '2. high': '223.4500',\n", 407 | " '3. low': '222.9000',\n", 408 | " '4. close': '223.3301',\n", 409 | " '5. volume': '289148'},\n", 410 | " '2024-11-05 14:20:00': {'1. open': '223.0300',\n", 411 | " '2. high': '223.0300',\n", 412 | " '3. low': '222.8450',\n", 413 | " '4. close': '222.9050',\n", 414 | " '5. volume': '143846'},\n", 415 | " '2024-11-05 14:15:00': {'1. open': '223.0100',\n", 416 | " '2. high': '223.1200',\n", 417 | " '3. low': '222.9600',\n", 418 | " '4. close': '223.0399',\n", 419 | " '5. volume': '114233'},\n", 420 | " '2024-11-05 14:10:00': {'1. open': '223.2300',\n", 421 | " '2. high': '223.2499',\n", 422 | " '3. low': '223.0000',\n", 423 | " '4. close': '223.0199',\n", 424 | " '5. volume': '135982'},\n", 425 | " '2024-11-05 14:05:00': {'1. open': '223.0300',\n", 426 | " '2. high': '223.2800',\n", 427 | " '3. low': '223.0150',\n", 428 | " '4. close': '223.2179',\n", 429 | " '5. volume': '167667'},\n", 430 | " '2024-11-05 14:00:00': {'1. open': '222.8600',\n", 431 | " '2. high': '223.1000',\n", 432 | " '3. low': '222.8316',\n", 433 | " '4. close': '223.0400',\n", 434 | " '5. volume': '172722'},\n", 435 | " '2024-11-05 13:55:00': {'1. open': '222.9100',\n", 436 | " '2. high': '222.9500',\n", 437 | " '3. low': '222.8500',\n", 438 | " '4. close': '222.8500',\n", 439 | " '5. volume': '157970'},\n", 440 | " '2024-11-05 13:50:00': {'1. open': '222.7500',\n", 441 | " '2. high': '223.0199',\n", 442 | " '3. low': '222.7300',\n", 443 | " '4. close': '222.9100',\n", 444 | " '5. volume': '166573'},\n", 445 | " '2024-11-05 13:45:00': {'1. open': '222.8200',\n", 446 | " '2. high': '222.9000',\n", 447 | " '3. low': '222.7300',\n", 448 | " '4. close': '222.7600',\n", 449 | " '5. volume': '123936'},\n", 450 | " '2024-11-05 13:40:00': {'1. open': '222.6400',\n", 451 | " '2. high': '222.8900',\n", 452 | " '3. low': '222.5250',\n", 453 | " '4. close': '222.8300',\n", 454 | " '5. volume': '165154'},\n", 455 | " '2024-11-05 13:35:00': {'1. open': '222.4800',\n", 456 | " '2. high': '222.6300',\n", 457 | " '3. low': '222.4002',\n", 458 | " '4. close': '222.6300',\n", 459 | " '5. volume': '239302'},\n", 460 | " '2024-11-05 13:30:00': {'1. open': '222.6300',\n", 461 | " '2. high': '222.8300',\n", 462 | " '3. low': '222.4600',\n", 463 | " '4. close': '222.4800',\n", 464 | " '5. volume': '265054'},\n", 465 | " '2024-11-05 13:25:00': {'1. open': '222.8200',\n", 466 | " '2. high': '222.9000',\n", 467 | " '3. low': '222.6200',\n", 468 | " '4. close': '222.6200',\n", 469 | " '5. volume': '251474'},\n", 470 | " '2024-11-05 13:20:00': {'1. open': '223.1200',\n", 471 | " '2. high': '223.1800',\n", 472 | " '3. low': '222.7700',\n", 473 | " '4. close': '222.8300',\n", 474 | " '5. volume': '174226'},\n", 475 | " '2024-11-05 13:15:00': {'1. open': '223.0500',\n", 476 | " '2. high': '223.2861',\n", 477 | " '3. low': '223.0100',\n", 478 | " '4. close': '223.1200',\n", 479 | " '5. volume': '195839'},\n", 480 | " '2024-11-05 13:10:00': {'1. open': '223.2086',\n", 481 | " '2. high': '223.2086',\n", 482 | " '3. low': '223.0000',\n", 483 | " '4. close': '223.0400',\n", 484 | " '5. volume': '184594'},\n", 485 | " '2024-11-05 13:05:00': {'1. open': '223.3800',\n", 486 | " '2. high': '223.4139',\n", 487 | " '3. low': '223.1720',\n", 488 | " '4. close': '223.2050',\n", 489 | " '5. volume': '182355'},\n", 490 | " '2024-11-05 13:00:00': {'1. open': '223.4950',\n", 491 | " '2. high': '223.5100',\n", 492 | " '3. low': '223.3000',\n", 493 | " '4. close': '223.3800',\n", 494 | " '5. volume': '152141'},\n", 495 | " '2024-11-05 12:55:00': {'1. open': '223.7300',\n", 496 | " '2. high': '223.7400',\n", 497 | " '3. low': '223.3800',\n", 498 | " '4. close': '223.4900',\n", 499 | " '5. volume': '152282'},\n", 500 | " '2024-11-05 12:50:00': {'1. open': '223.7300',\n", 501 | " '2. high': '223.7800',\n", 502 | " '3. low': '223.6303',\n", 503 | " '4. close': '223.7373',\n", 504 | " '5. volume': '124886'},\n", 505 | " '2024-11-05 12:45:00': {'1. open': '223.5700',\n", 506 | " '2. high': '223.7399',\n", 507 | " '3. low': '223.4800',\n", 508 | " '4. close': '223.7200',\n", 509 | " '5. volume': '148196'},\n", 510 | " '2024-11-05 12:40:00': {'1. open': '223.4700',\n", 511 | " '2. high': '223.6350',\n", 512 | " '3. low': '223.4600',\n", 513 | " '4. close': '223.5600',\n", 514 | " '5. volume': '327187'},\n", 515 | " '2024-11-05 12:35:00': {'1. open': '223.5300',\n", 516 | " '2. high': '223.6200',\n", 517 | " '3. low': '223.3820',\n", 518 | " '4. close': '223.4700',\n", 519 | " '5. volume': '118840'},\n", 520 | " '2024-11-05 12:30:00': {'1. open': '223.4000',\n", 521 | " '2. high': '223.6500',\n", 522 | " '3. low': '223.3800',\n", 523 | " '4. close': '223.5300',\n", 524 | " '5. volume': '135782'},\n", 525 | " '2024-11-05 12:25:00': {'1. open': '223.6500',\n", 526 | " '2. high': '223.7500',\n", 527 | " '3. low': '223.3100',\n", 528 | " '4. close': '223.3800',\n", 529 | " '5. volume': '190068'},\n", 530 | " '2024-11-05 12:20:00': {'1. open': '223.6750',\n", 531 | " '2. high': '223.6950',\n", 532 | " '3. low': '223.4350',\n", 533 | " '4. close': '223.6500',\n", 534 | " '5. volume': '161559'},\n", 535 | " '2024-11-05 12:15:00': {'1. open': '223.7000',\n", 536 | " '2. high': '223.7500',\n", 537 | " '3. low': '223.6000',\n", 538 | " '4. close': '223.6700',\n", 539 | " '5. volume': '132319'},\n", 540 | " '2024-11-05 12:10:00': {'1. open': '223.4100',\n", 541 | " '2. high': '223.7400',\n", 542 | " '3. low': '223.3200',\n", 543 | " '4. close': '223.6950',\n", 544 | " '5. volume': '229604'},\n", 545 | " '2024-11-05 12:05:00': {'1. open': '223.3050',\n", 546 | " '2. high': '223.4400',\n", 547 | " '3. low': '223.2600',\n", 548 | " '4. close': '223.4100',\n", 549 | " '5. volume': '175482'},\n", 550 | " '2024-11-05 12:00:00': {'1. open': '223.2500',\n", 551 | " '2. high': '223.3300',\n", 552 | " '3. low': '223.1500',\n", 553 | " '4. close': '223.3099',\n", 554 | " '5. volume': '190376'},\n", 555 | " '2024-11-05 11:55:00': {'1. open': '223.2400',\n", 556 | " '2. high': '223.3300',\n", 557 | " '3. low': '223.1600',\n", 558 | " '4. close': '223.2300',\n", 559 | " '5. volume': '171788'},\n", 560 | " '2024-11-05 11:50:00': {'1. open': '223.0400',\n", 561 | " '2. high': '223.2600',\n", 562 | " '3. low': '222.9103',\n", 563 | " '4. close': '223.2400',\n", 564 | " '5. volume': '160700'},\n", 565 | " '2024-11-05 11:45:00': {'1. open': '222.8000',\n", 566 | " '2. high': '223.1200',\n", 567 | " '3. low': '222.8000',\n", 568 | " '4. close': '223.0401',\n", 569 | " '5. volume': '164338'},\n", 570 | " '2024-11-05 11:40:00': {'1. open': '223.0500',\n", 571 | " '2. high': '223.0522',\n", 572 | " '3. low': '222.7500',\n", 573 | " '4. close': '222.7800',\n", 574 | " '5. volume': '143921'}}}" 575 | ] 576 | }, 577 | "execution_count": 7, 578 | "metadata": {}, 579 | "output_type": "execute_result" 580 | } 581 | ], 582 | "source": [ 583 | "data" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": null, 589 | "id": "149c958e-0ee5-4fb5-9420-0144a7fa9154", 590 | "metadata": {}, 591 | "outputs": [], 592 | "source": [] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "id": "29022f7a-9f92-456d-b678-be2f0b4ab565", 598 | "metadata": {}, 599 | "outputs": [], 600 | "source": [] 601 | } 602 | ], 603 | "metadata": { 604 | "kernelspec": { 605 | "display_name": "Python 3 (ipykernel)", 606 | "language": "python", 607 | "name": "python3" 608 | }, 609 | "language_info": { 610 | "codemirror_mode": { 611 | "name": "ipython", 612 | "version": 3 613 | }, 614 | "file_extension": ".py", 615 | "mimetype": "text/x-python", 616 | "name": "python", 617 | "nbconvert_exporter": "python", 618 | "pygments_lexer": "ipython3", 619 | "version": "3.12.3" 620 | } 621 | }, 622 | "nbformat": 4, 623 | "nbformat_minor": 5 624 | } 625 | -------------------------------------------------------------------------------- /nbs/8 - Django Setup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "a3af9e50-7900-4b8a-a8d0-19a6e6accedd", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from django.conf import settings" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "3ce281ab-ec09-46a4-8cc1-6fd017658504", 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "data": { 32 | "text/plain": [ 33 | "PosixPath('/Users/cfe/Dev/stock-trading-bot/src')" 34 | ] 35 | }, 36 | "execution_count": 3, 37 | "metadata": {}, 38 | "output_type": "execute_result" 39 | } 40 | ], 41 | "source": [ 42 | "settings.BASE_DIR" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "id": "1c41c17d-e1c8-4488-910b-30779a706b81", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "from django.contrib.auth import get_user_model\n", 53 | "\n", 54 | "User = get_user_model()" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 5, 60 | "id": "998f6c5f-18ed-4829-ad74-af5b039520a4", 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "text/plain": [ 66 | "]>" 67 | ] 68 | }, 69 | "execution_count": 5, 70 | "metadata": {}, 71 | "output_type": "execute_result" 72 | } 73 | ], 74 | "source": [ 75 | "User.objects.all()" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "id": "da110c89-fc4d-4751-840b-2a95891c50bf", 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "id": "1984f4ab-2d37-4484-a161-ddd977b34cf4", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [] 93 | } 94 | ], 95 | "metadata": { 96 | "kernelspec": { 97 | "display_name": "Python 3 (ipykernel)", 98 | "language": "python", 99 | "name": "python3" 100 | }, 101 | "language_info": { 102 | "codemirror_mode": { 103 | "name": "ipython", 104 | "version": 3 105 | }, 106 | "file_extension": ".py", 107 | "mimetype": "text/x-python", 108 | "name": "python", 109 | "nbconvert_exporter": "python", 110 | "pygments_lexer": "ipython3", 111 | "version": "3.12.3" 112 | } 113 | }, 114 | "nbformat": 4, 115 | "nbformat_minor": 5 116 | } 117 | -------------------------------------------------------------------------------- /nbs/Analyze - 1 - Moving Averages.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market.models import StockQuote" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "31ffb14d-43f3-4bbb-813b-50526ff9bfd6", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from django.db.models import Avg, F, RowRange, Window\n", 32 | "from django.utils import timezone\n", 33 | "from datetime import timedelta\n", 34 | "from decimal import Decimal" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 4, 40 | "id": "cee627e2-6fe5-4b27-9027-8e07ffbe24b8", 41 | "metadata": {}, 42 | "outputs": [ 43 | { 44 | "data": { 45 | "text/plain": [ 46 | "18980" 47 | ] 48 | }, 49 | "execution_count": 4, 50 | "metadata": {}, 51 | "output_type": "execute_result" 52 | } 53 | ], 54 | "source": [ 55 | "days_ago = 30\n", 56 | "now = timezone.now()\n", 57 | "start_date = now - timedelta(days=30)\n", 58 | "end_date = now\n", 59 | "\n", 60 | "qs = StockQuote.objects.filter(time__range=(start_date, end_date))\n", 61 | "qs.count()" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 5, 67 | "id": "0415473f-c008-4f14-a37e-ca6f264313d6", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "total = 0\n", 72 | "for obj in qs:\n", 73 | " total += obj.close_price" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 6, 79 | "id": "41e0ac06-3e07-42ac-b781-f154ce9d3ca7", 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "data": { 84 | "text/plain": [ 85 | "Decimal('301.3669428503688092729188620')" 86 | ] 87 | }, 88 | "execution_count": 6, 89 | "metadata": {}, 90 | "output_type": "execute_result" 91 | } 92 | ], 93 | "source": [ 94 | "total / qs.count()" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 7, 100 | "id": "50cbcb1c-a9c6-45c3-b8f2-177f19d7e995", 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "data": { 105 | "text/plain": [ 106 | "" 107 | ] 108 | }, 109 | "execution_count": 7, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "qs.values('company').annotate(avg_price=Avg('close_price'))" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 8, 121 | "id": "a8f1344a-5eb7-4eab-b785-2814f24acf48", 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "count = 5\n", 126 | "ticker = \"AAPL\"\n", 127 | "rolling_qs = list(qs.filter(company__ticker=ticker).order_by('-time')[:count])\n", 128 | "rolling_qs.reverse()" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 9, 134 | "id": "2daeab04-616c-4923-b866-3d2900a17997", 135 | "metadata": {}, 136 | "outputs": [ 137 | { 138 | "data": { 139 | "text/plain": [ 140 | "[,\n", 141 | " ,\n", 142 | " ,\n", 143 | " ,\n", 144 | " ]" 145 | ] 146 | }, 147 | "execution_count": 9, 148 | "metadata": {}, 149 | "output_type": "execute_result" 150 | } 151 | ], 152 | "source": [ 153 | "rolling_qs" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 10, 159 | "id": "25e4ee18-7b1f-4a7c-b8e5-6e1c7586ce62", 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "name": "stdout", 164 | "output_type": "stream", 165 | "text": [ 166 | "1 6466357 226.4512 226.4512\n", 167 | "2 6466358 226.3638 226.4075\n", 168 | "3 6466359 225.9787 226.2645666666666666666666667\n", 169 | "4 6466360 225.9750 226.192175\n", 170 | "5 6466361 225.9900 226.15174\n" 171 | ] 172 | } 173 | ], 174 | "source": [ 175 | "total = 0\n", 176 | "for i, obj in enumerate(rolling_qs):\n", 177 | " total += obj.close_price\n", 178 | " avg = total / (i + 1)\n", 179 | " print(i + 1, obj.id, obj.close_price, avg)" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 11, 185 | "id": "5a64b03f-7724-4253-a412-95a98ac1ff35", 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "qs = StockQuote.objects.filter(company__ticker=ticker, time__range=(start_date, end_date))" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 12, 195 | "id": "1586782f-2759-49eb-ae74-6611c1626813", 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "frame_start = -(count - 1)\n", 200 | "ma_val = qs.annotate(\n", 201 | " ma=Window(\n", 202 | " expression=Avg('close_price'),\n", 203 | " order_by=F('time').asc(),\n", 204 | " partition_by=[],\n", 205 | " frame=RowRange(start=frame_start, end=0),\n", 206 | " )\n", 207 | ").order_by('-time')" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 13, 213 | "id": "76e23c23-e0ec-4d1a-b5e4-0fe89d09643a", 214 | "metadata": {}, 215 | "outputs": [ 216 | { 217 | "name": "stdout", 218 | "output_type": "stream", 219 | "text": [ 220 | "6466361 225.9900 226.1517400000000000 2024-11-13 18:40:00+00:00\n", 221 | "6466360 225.9750 226.2457400000000000 2024-11-13 18:35:00+00:00\n", 222 | "6466359 225.9787 226.3627400000000000 2024-11-13 18:30:00+00:00\n", 223 | "6466358 226.3638 226.4130000000000000 2024-11-13 18:25:00+00:00\n", 224 | "6466357 226.4512 226.4692400000000000 2024-11-13 18:20:00+00:00\n" 225 | ] 226 | } 227 | ], 228 | "source": [ 229 | "for obj in ma_val[:5]:\n", 230 | " print(obj.id, obj.close_price, obj.ma, obj.time)" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 14, 236 | "id": "2cce51aa-361e-47d2-9fbf-45dad2617707", 237 | "metadata": {}, 238 | "outputs": [ 239 | { 240 | "data": { 241 | "text/plain": [ 242 | "(6466361,\n", 243 | " Decimal('225.9900'),\n", 244 | " Decimal('226.1517400000000000'),\n", 245 | " Decimal('225.9005650000000000'))" 246 | ] 247 | }, 248 | "execution_count": 14, 249 | "metadata": {}, 250 | "output_type": "execute_result" 251 | } 252 | ], 253 | "source": [ 254 | "frame_start = -(count - 1)\n", 255 | "ma_vals = qs.annotate(\n", 256 | " ma_5=Window(\n", 257 | " expression=Avg('close_price'),\n", 258 | " order_by=F('time').asc(),\n", 259 | " partition_by=[],\n", 260 | " frame=RowRange(start=-4, end=0),\n", 261 | " ),\n", 262 | " ma_20=Window(\n", 263 | " expression=Avg('close_price'),\n", 264 | " order_by=F('time').asc(),\n", 265 | " partition_by=[],\n", 266 | " frame=RowRange(start=-19, end=0),\n", 267 | " )\n", 268 | ").order_by('-time').first()\n", 269 | "\n", 270 | "ma_vals.id, ma_vals.close_price, ma_vals.ma_5, ma_vals.ma_20" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "id": "9ee11312-df57-4bd1-bee6-1d4e2a94d00d", 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [] 280 | } 281 | ], 282 | "metadata": { 283 | "kernelspec": { 284 | "display_name": "Python 3 (ipykernel)", 285 | "language": "python", 286 | "name": "python3" 287 | }, 288 | "language_info": { 289 | "codemirror_mode": { 290 | "name": "ipython", 291 | "version": 3 292 | }, 293 | "file_extension": ".py", 294 | "mimetype": "text/x-python", 295 | "name": "python", 296 | "nbconvert_exporter": "python", 297 | "pygments_lexer": "ipython3", 298 | "version": "3.12.3" 299 | } 300 | }, 301 | "nbformat": 4, 302 | "nbformat_minor": 5 303 | } 304 | -------------------------------------------------------------------------------- /nbs/Analyze - 2 - Daily Moving Averages.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market.models import StockQuote" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "31ffb14d-43f3-4bbb-813b-50526ff9bfd6", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from django.db.models import Avg, F, RowRange, Window, Max\n", 32 | "from django.db.models.functions import TruncDate\n", 33 | "from django.utils import timezone\n", 34 | "from datetime import timedelta\n", 35 | "from decimal import Decimal" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 4, 41 | "id": "cee627e2-6fe5-4b27-9027-8e07ffbe24b8", 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "data": { 46 | "text/plain": [ 47 | "19214" 48 | ] 49 | }, 50 | "execution_count": 4, 51 | "metadata": {}, 52 | "output_type": "execute_result" 53 | } 54 | ], 55 | "source": [ 56 | "days_ago = 30\n", 57 | "now = timezone.now()\n", 58 | "start_date = now - timedelta(days=30)\n", 59 | "end_date = now\n", 60 | "\n", 61 | "qs = StockQuote.objects.filter(time__range=(start_date, end_date))\n", 62 | "qs.count()" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 5, 68 | "id": "0415473f-c008-4f14-a37e-ca6f264313d6", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "total = 0\n", 73 | "for obj in qs:\n", 74 | " total += obj.close_price" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 6, 80 | "id": "41e0ac06-3e07-42ac-b781-f154ce9d3ca7", 81 | "metadata": {}, 82 | "outputs": [ 83 | { 84 | "data": { 85 | "text/plain": [ 86 | "Decimal('301.3717889143332986364109503')" 87 | ] 88 | }, 89 | "execution_count": 6, 90 | "metadata": {}, 91 | "output_type": "execute_result" 92 | } 93 | ], 94 | "source": [ 95 | "total / qs.count()" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 7, 101 | "id": "50cbcb1c-a9c6-45c3-b8f2-177f19d7e995", 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "text/plain": [ 107 | "" 108 | ] 109 | }, 110 | "execution_count": 7, 111 | "metadata": {}, 112 | "output_type": "execute_result" 113 | } 114 | ], 115 | "source": [ 116 | "qs.values('company').annotate(avg_price=Avg('close_price'))" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 8, 122 | "id": "a8f1344a-5eb7-4eab-b785-2814f24acf48", 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "count = 5\n", 127 | "ticker = \"AAPL\"\n", 128 | "rolling_qs = list(qs.filter(company__ticker=ticker).order_by('-time')[:count])\n", 129 | "rolling_qs.reverse()" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": 9, 135 | "id": "2daeab04-616c-4923-b866-3d2900a17997", 136 | "metadata": {}, 137 | "outputs": [ 138 | { 139 | "data": { 140 | "text/plain": [ 141 | "[,\n", 142 | " ,\n", 143 | " ,\n", 144 | " ,\n", 145 | " ]" 146 | ] 147 | }, 148 | "execution_count": 9, 149 | "metadata": {}, 150 | "output_type": "execute_result" 151 | } 152 | ], 153 | "source": [ 154 | "rolling_qs" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 10, 160 | "id": "25e4ee18-7b1f-4a7c-b8e5-6e1c7586ce62", 161 | "metadata": {}, 162 | "outputs": [ 163 | { 164 | "name": "stdout", 165 | "output_type": "stream", 166 | "text": [ 167 | "1 8621061 225.1500 225.1500\n", 168 | "2 8621062 225.0200 225.0850\n", 169 | "3 8621063 224.9800 225.0500\n", 170 | "4 8621064 225.0000 225.0375\n", 171 | "5 8621065 225.0000 225.0300\n" 172 | ] 173 | } 174 | ], 175 | "source": [ 176 | "total = 0\n", 177 | "for i, obj in enumerate(rolling_qs):\n", 178 | " total += obj.close_price\n", 179 | " avg = total / (i + 1)\n", 180 | " print(i + 1, obj.id, obj.close_price, avg)" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 33, 186 | "id": "d93c0176-d3fa-4db6-b47d-ced32a6d2507", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "lastest_daily_timestamps = (\n", 191 | " StockQuote.objects.filter(company__ticker=ticker, time__range=(start_date - timedelta(days=40), end_date))\n", 192 | " .annotate(date=TruncDate('time'))\n", 193 | " .values('company', 'date')\n", 194 | " .annotate(latest_time=Max('time'))\n", 195 | " .values('company', 'date', 'latest_time')\n", 196 | " .order_by('date')\n", 197 | ")\n", 198 | "\n", 199 | "acutal_timestamps = [x['latest_time'] for x in lastest_daily_timestamps]" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": 34, 205 | "id": "5a64b03f-7724-4253-a412-95a98ac1ff35", 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "qs = StockQuote.objects.filter(\n", 210 | " company__ticker=ticker, \n", 211 | " time__range=(start_date, end_date),\n", 212 | " time__in=acutal_timestamps\n", 213 | ")" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 35, 219 | "id": "1586782f-2759-49eb-ae74-6611c1626813", 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "frame_start = -(count - 1)\n", 224 | "ma_val = qs.annotate(\n", 225 | " ma=Window(\n", 226 | " expression=Avg('close_price'),\n", 227 | " order_by=F('time').asc(),\n", 228 | " partition_by=[],\n", 229 | " frame=RowRange(start=frame_start, end=0),\n", 230 | " )\n", 231 | ").order_by('-time')" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 36, 237 | "id": "76e23c23-e0ec-4d1a-b5e4-0fe89d09643a", 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "name": "stdout", 242 | "output_type": "stream", 243 | "text": [ 244 | "8621065 225.0000 225.4460000000000000 2024-11-13 23:25:00+00:00\n", 245 | "6476367 224.2000 225.9420200000000000 2024-11-12 23:50:00+00:00\n", 246 | "6476181 224.1100 225.6320200000000000 2024-11-11 23:55:00+00:00\n", 247 | "6503221 226.9600 225.4020200000000000 2024-11-09 00:55:00+00:00\n", 248 | "6503204 226.9600 224.3500200000000000 2024-11-08 23:55:00+00:00\n" 249 | ] 250 | } 251 | ], 252 | "source": [ 253 | "for obj in ma_val[:5]:\n", 254 | " print(obj.id, obj.close_price, obj.ma, obj.time)" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 37, 260 | "id": "2cce51aa-361e-47d2-9fbf-45dad2617707", 261 | "metadata": {}, 262 | "outputs": [ 263 | { 264 | "data": { 265 | "text/plain": [ 266 | "(8621065,\n", 267 | " Decimal('225.0000'),\n", 268 | " Decimal('225.4460000000000000'),\n", 269 | " Decimal('228.0193950000000000'))" 270 | ] 271 | }, 272 | "execution_count": 37, 273 | "metadata": {}, 274 | "output_type": "execute_result" 275 | } 276 | ], 277 | "source": [ 278 | "frame_start = -(count - 1)\n", 279 | "ma_vals = qs.annotate(\n", 280 | " ma_5=Window(\n", 281 | " expression=Avg('close_price'),\n", 282 | " order_by=F('time').asc(),\n", 283 | " partition_by=[],\n", 284 | " frame=RowRange(start=-4, end=0),\n", 285 | " ),\n", 286 | " ma_20=Window(\n", 287 | " expression=Avg('close_price'),\n", 288 | " order_by=F('time').asc(),\n", 289 | " partition_by=[],\n", 290 | " frame=RowRange(start=-19, end=0),\n", 291 | " )\n", 292 | ").order_by('-time').first()\n", 293 | "\n", 294 | "ma_vals.id, ma_vals.close_price, ma_vals.ma_5, ma_vals.ma_20" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "id": "9ee11312-df57-4bd1-bee6-1d4e2a94d00d", 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "id": "b2f959d6-8687-4f08-8789-01a93104704e", 309 | "metadata": {}, 310 | "outputs": [], 311 | "source": [] 312 | } 313 | ], 314 | "metadata": { 315 | "kernelspec": { 316 | "display_name": "Python 3 (ipykernel)", 317 | "language": "python", 318 | "name": "python3" 319 | }, 320 | "language_info": { 321 | "codemirror_mode": { 322 | "name": "ipython", 323 | "version": 3 324 | }, 325 | "file_extension": ".py", 326 | "mimetype": "text/x-python", 327 | "name": "python", 328 | "nbconvert_exporter": "python", 329 | "pygments_lexer": "ipython3", 330 | "version": "3.12.3" 331 | } 332 | }, 333 | "nbformat": 4, 334 | "nbformat_minor": 5 335 | } 336 | -------------------------------------------------------------------------------- /nbs/Analyze - 3 - Volume Trend and Price Target.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market.models import StockQuote" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "31ffb14d-43f3-4bbb-813b-50526ff9bfd6", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from django.db.models import Avg, F, RowRange, Window, Max, Min\n", 32 | "from django.db.models.functions import TruncDate, FirstValue\n", 33 | "from django.utils import timezone\n", 34 | "from datetime import timedelta\n", 35 | "from decimal import Decimal" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 4, 41 | "id": "cee627e2-6fe5-4b27-9027-8e07ffbe24b8", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "days_ago = 30\n", 46 | "now = timezone.now()\n", 47 | "start_date = now - timedelta(days=30)\n", 48 | "end_date = now\n", 49 | "ticker = \"AAPL\"" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 5, 55 | "id": "d93c0176-d3fa-4db6-b47d-ced32a6d2507", 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "data": { 60 | "text/plain": [ 61 | "51" 62 | ] 63 | }, 64 | "execution_count": 5, 65 | "metadata": {}, 66 | "output_type": "execute_result" 67 | } 68 | ], 69 | "source": [ 70 | "lastest_daily_timestamps = (\n", 71 | " StockQuote.timescale.filter(company__ticker=ticker, time__range=(start_date - timedelta(days=40), end_date))\n", 72 | " .time_bucket('time', '1 day')\n", 73 | " .annotate(date=TruncDate('time'))\n", 74 | " .values('company', 'date')\n", 75 | " .annotate(latest_time=Max('time'))\n", 76 | " .values('company', 'date', 'latest_time')\n", 77 | " .order_by('date')\n", 78 | ")\n", 79 | "\n", 80 | "acutal_timestamps = list(set([x['latest_time'] for x in lastest_daily_timestamps]))\n", 81 | "len(acutal_timestamps)" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 20, 87 | "id": "5a64b03f-7724-4253-a412-95a98ac1ff35", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "qs = StockQuote.timescale.filter(\n", 92 | " company__ticker=ticker, \n", 93 | " time__range=(start_date, end_date),\n", 94 | " time__in=acutal_timestamps\n", 95 | ")" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 21, 101 | "id": "1586782f-2759-49eb-ae74-6611c1626813", 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "def get_volume_trend(queryset, days=5):\n", 106 | " \"\"\"\n", 107 | " Analyze recent volume trends\n", 108 | " \"\"\"\n", 109 | " start = -(days - 1)\n", 110 | " data = queryset.annotate(\n", 111 | " avg_volume=Window(\n", 112 | " expression=Avg('volume'),\n", 113 | " order_by=F('time').asc(),\n", 114 | " partition_by=[],\n", 115 | " frame=RowRange(start=start, end=0)\n", 116 | " )\n", 117 | " ).order_by('-time').first()\n", 118 | "\n", 119 | " if not data:\n", 120 | " return None\n", 121 | " volume_change = ((data.volume - data.avg_volume) / \n", 122 | " data.avg_volume) * 100\n", 123 | " return {\n", 124 | " 'avg_volume': float(data.avg_volume),\n", 125 | " 'latest_volume': int(data.volume),\n", 126 | " 'volume_change_percent': float(volume_change)\n", 127 | " }" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 22, 133 | "id": "99dc15ab-63f9-4bbc-8377-043c8a669a71", 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "{'avg_volume': 488.0,\n", 140 | " 'latest_volume': 304,\n", 141 | " 'volume_change_percent': -37.704918032786885}" 142 | ] 143 | }, 144 | "execution_count": 22, 145 | "metadata": {}, 146 | "output_type": "execute_result" 147 | } 148 | ], 149 | "source": [ 150 | "get_volume_trend(qs)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 11, 156 | "id": "76e23c23-e0ec-4d1a-b5e4-0fe89d09643a", 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "def get_simple_target(ticker, timestamps=[], days=180):\n", 161 | " \"\"\"\n", 162 | " Simplified price target calculation\n", 163 | " \"\"\"\n", 164 | " end_date = timezone.now()\n", 165 | " start_date = end_date - timedelta(days=days)\n", 166 | " lookups = {\n", 167 | " \"company__ticker\": ticker,\n", 168 | " \"time__range\": (start_date, end_date)\n", 169 | " }\n", 170 | " if len(timestamps) > 0:\n", 171 | " lookups['time__in'] = timestamps\n", 172 | " daily_data = (\n", 173 | " StockQuote.timescale\n", 174 | " .filter(**lookups)\n", 175 | " .time_bucket('time', '1 day')\n", 176 | " .annotate(\n", 177 | " latest_price=Window(\n", 178 | " expression=FirstValue('close_price'),\n", 179 | " partition_by=[],\n", 180 | " order_by=F('time').desc()\n", 181 | " )\n", 182 | " )\n", 183 | " .aggregate(\n", 184 | " current_price=Max('latest_price'),\n", 185 | " avg_price=Avg('close_price'),\n", 186 | " highest=Max('high_price'),\n", 187 | " lowest=Min('low_price')\n", 188 | " )\n", 189 | " )\n", 190 | " \n", 191 | " if not daily_data:\n", 192 | " return None\n", 193 | " \n", 194 | " current_price = float(daily_data['current_price'])\n", 195 | " avg_price = float(daily_data['avg_price'])\n", 196 | " price_range = float(daily_data['highest']) - float(daily_data['lowest'])\n", 197 | " \n", 198 | " # Simple target based on average price and recent range\n", 199 | " conservative_target = current_price + (price_range * 0.382) # 38.2% Fibonacci\n", 200 | " aggressive_target = current_price + (price_range * 0.618) # 61.8% Fibonacci\n", 201 | " \n", 202 | " return {\n", 203 | " 'current_price': current_price,\n", 204 | " 'conservative_target': conservative_target,\n", 205 | " 'aggressive_target': aggressive_target,\n", 206 | " 'average_price': avg_price\n", 207 | " }" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 23, 213 | "id": "9ee11312-df57-4bd1-bee6-1d4e2a94d00d", 214 | "metadata": {}, 215 | "outputs": [ 216 | { 217 | "data": { 218 | "text/plain": [ 219 | "{'current_price': 225.0,\n", 220 | " 'conservative_target': 244.43043,\n", 221 | " 'aggressive_target': 256.43457,\n", 222 | " 'average_price': 219.53152809254635}" 223 | ] 224 | }, 225 | "execution_count": 23, 226 | "metadata": {}, 227 | "output_type": "execute_result" 228 | } 229 | ], 230 | "source": [ 231 | "get_simple_target(\"AAPL\")" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 24, 237 | "id": "b2f959d6-8687-4f08-8789-01a93104704e", 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "data": { 242 | "text/plain": [ 243 | "{'current_price': 225.0,\n", 244 | " 'conservative_target': 244.43043,\n", 245 | " 'aggressive_target': 256.43457,\n", 246 | " 'average_price': 219.53152809254635}" 247 | ] 248 | }, 249 | "execution_count": 24, 250 | "metadata": {}, 251 | "output_type": "execute_result" 252 | } 253 | ], 254 | "source": [ 255 | "# get_simple_target(\"AAPL\")" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "id": "6ca3de8b-c2a8-44de-a6c7-6d5c1c6813a1", 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [] 265 | } 266 | ], 267 | "metadata": { 268 | "kernelspec": { 269 | "display_name": "Python 3 (ipykernel)", 270 | "language": "python", 271 | "name": "python3" 272 | }, 273 | "language_info": { 274 | "codemirror_mode": { 275 | "name": "ipython", 276 | "version": 3 277 | }, 278 | "file_extension": ".py", 279 | "mimetype": "text/x-python", 280 | "name": "python", 281 | "nbconvert_exporter": "python", 282 | "pygments_lexer": "ipython3", 283 | "version": "3.12.3" 284 | } 285 | }, 286 | "nbformat": 4, 287 | "nbformat_minor": 5 288 | } 289 | -------------------------------------------------------------------------------- /nbs/Analyze - 4 - Analyze Stocks - Relative Strength Index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 9, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 10, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market.models import StockQuote" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 11, 27 | "id": "31ffb14d-43f3-4bbb-813b-50526ff9bfd6", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from django.db.models import (\n", 32 | " Avg, \n", 33 | " F,\n", 34 | " RowRange,\n", 35 | " Window,\n", 36 | " Max,\n", 37 | " Min,\n", 38 | " ExpressionWrapper,\n", 39 | " DecimalField,\n", 40 | " Case,\n", 41 | " When,\n", 42 | " Value\n", 43 | ")\n", 44 | "from django.db.models.functions import TruncDate, FirstValue, Lag, Coalesce\n", 45 | "from django.utils import timezone\n", 46 | "from datetime import timedelta\n", 47 | "from decimal import Decimal" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 12, 53 | "id": "6ca3de8b-c2a8-44de-a6c7-6d5c1c6813a1", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "def calculate_rsi(ticker, period=14):\n", 58 | " \"\"\"\n", 59 | " Calculate Relative Strength Index (RSI) using Django ORM.\n", 60 | " \n", 61 | " Args:\n", 62 | " ticker (str): Stock ticker symbol\n", 63 | " period (int): RSI period (default: 14)\n", 64 | " \n", 65 | " Returns:\n", 66 | " dict: RSI value and component calculations\n", 67 | " \"\"\"\n", 68 | " end_date = timezone.now()\n", 69 | " start_date = end_date - timedelta(days=period * 4)\n", 70 | " \n", 71 | " # Get daily price data\n", 72 | " daily_data = (\n", 73 | " StockQuote.timescale\n", 74 | " .filter(company__ticker=ticker, time__range=(start_date, end_date))\n", 75 | " .time_bucket('time', '1 day')\n", 76 | " .order_by('bucket')\n", 77 | " )\n", 78 | " \n", 79 | " # Calculate price changes and gains/losses with explicit decimal conversion\n", 80 | " movement = daily_data.annotate(\n", 81 | " closing_price=ExpressionWrapper(\n", 82 | " F('close_price'),\n", 83 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 84 | " ),\n", 85 | " prev_close=Window(\n", 86 | " expression=Lag('close_price'),\n", 87 | " order_by=F('bucket').asc(),\n", 88 | " partition_by=[],\n", 89 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 90 | " )\n", 91 | " ).annotate(\n", 92 | " price_change=ExpressionWrapper(\n", 93 | " F('close_price') - F('prev_close'),\n", 94 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 95 | " ),\n", 96 | " gain=Case(\n", 97 | " When(price_change__gt=0, \n", 98 | " then=ExpressionWrapper(\n", 99 | " F('price_change'),\n", 100 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 101 | " )),\n", 102 | " default=Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)),\n", 103 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 104 | " ),\n", 105 | " loss=Case(\n", 106 | " When(price_change__lt=0,\n", 107 | " then=ExpressionWrapper(\n", 108 | " -F('price_change'),\n", 109 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 110 | " )),\n", 111 | " default=Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)),\n", 112 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 113 | " )\n", 114 | " )\n", 115 | " \n", 116 | " # Calculate initial averages for the first period\n", 117 | " initial_avg = movement.exclude(prev_close__isnull=True)[:period].aggregate(\n", 118 | " avg_gain=Coalesce(\n", 119 | " ExpressionWrapper(\n", 120 | " Avg('gain'),\n", 121 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 122 | " ),\n", 123 | " Value(0, output_field=DecimalField(max_digits=10, decimal_places=4))\n", 124 | " ),\n", 125 | " avg_loss=Coalesce(\n", 126 | " ExpressionWrapper(\n", 127 | " Avg('loss'),\n", 128 | " output_field=DecimalField(max_digits=10, decimal_places=4)\n", 129 | " ),\n", 130 | " Value(0, output_field=DecimalField(max_digits=10, decimal_places=4))\n", 131 | " )\n", 132 | " )\n", 133 | " \n", 134 | " # Get subsequent data points for EMA calculation\n", 135 | " subsequent_data = list(movement.exclude(prev_close__isnull=True)[period:].values('gain', 'loss'))\n", 136 | " \n", 137 | " # Calculate EMA-based RSI\n", 138 | " avg_gain = initial_avg['avg_gain']\n", 139 | " avg_loss = initial_avg['avg_loss']\n", 140 | " alpha = Decimal(1 / period) # Smoothing factor\n", 141 | " \n", 142 | " # Update moving averages using EMA formula\n", 143 | " for data in subsequent_data:\n", 144 | " avg_gain = (avg_gain * (1 - alpha) + data['gain'] * alpha)\n", 145 | " avg_loss = (avg_loss * (1 - alpha) + data['loss'] * alpha)\n", 146 | " \n", 147 | " # Prevent division by zero\n", 148 | " if avg_loss == 0:\n", 149 | " rsi = 100\n", 150 | " else:\n", 151 | " rs = avg_gain / avg_loss\n", 152 | " rsi = 100 - (100 / (1 + rs))\n", 153 | " \n", 154 | " return {\n", 155 | " 'rsi': round(float(rsi), 4),\n", 156 | " 'avg_gain': round(float(avg_gain), 4),\n", 157 | " 'avg_loss': round(float(avg_loss), 4),\n", 158 | " 'period': period\n", 159 | " }" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 13, 165 | "id": "8bc44c5c-99f4-4b12-ac6e-1fd4c3805604", 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "rsi_data = calculate_rsi('AAPL')" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 14, 175 | "id": "df5a66b1-34bc-414d-a146-90cb63b96e62", 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "data": { 180 | "text/plain": [ 181 | "{'rsi': 52.3555, 'avg_gain': 0.0509, 'avg_loss': 0.0464, 'period': 14}" 182 | ] 183 | }, 184 | "execution_count": 14, 185 | "metadata": {}, 186 | "output_type": "execute_result" 187 | } 188 | ], 189 | "source": [ 190 | "rsi_data" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "id": "92ad1df1-ae1f-411f-aa4b-48f854015315", 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [] 200 | } 201 | ], 202 | "metadata": { 203 | "kernelspec": { 204 | "display_name": "Python 3 (ipykernel)", 205 | "language": "python", 206 | "name": "python3" 207 | }, 208 | "language_info": { 209 | "codemirror_mode": { 210 | "name": "ipython", 211 | "version": 3 212 | }, 213 | "file_extension": ".py", 214 | "mimetype": "text/x-python", 215 | "name": "python", 216 | "nbconvert_exporter": "python", 217 | "pygments_lexer": "ipython3", 218 | "version": "3.12.3" 219 | } 220 | }, 221 | "nbformat": 4, 222 | "nbformat_minor": 5 223 | } 224 | -------------------------------------------------------------------------------- /nbs/Analyze - 5 - Stocks Services.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market import services as market_services" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "b0d27cf2-6c7c-4dfa-9425-6da85d9a0e32", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "ticker = \"AAPL\"\n", 32 | "days = 28\n", 33 | "queryset = market_services.get_daily_stock_quotes_queryset(ticker, days=days)\n", 34 | "averages = market_services.get_daily_moving_averages(ticker, days=days, queryset=queryset)\n", 35 | "price_target = market_services.get_price_target(ticker, days=days, queryset=queryset)\n", 36 | "volume_trend = market_services.get_volume_trend(ticker, days=days, queryset=queryset)\n", 37 | "rsi = market_services.calculate_rsi(ticker, days=days, period=14)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "id": "83de4c5c-f2f6-401e-92d1-be8dd85e7871", 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "data": { 48 | "text/plain": [ 49 | "22" 50 | ] 51 | }, 52 | "execution_count": 4, 53 | "metadata": {}, 54 | "output_type": "execute_result" 55 | } 56 | ], 57 | "source": [ 58 | "queryset.count()" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 5, 64 | "id": "40d78422-aa5f-4361-807c-6fc1fba3e8ac", 65 | "metadata": {}, 66 | "outputs": [ 67 | { 68 | "data": { 69 | "text/plain": [ 70 | "{'ma_5': Decimal('224.9920'), 'ma_20': Decimal('227.5094')}" 71 | ] 72 | }, 73 | "execution_count": 5, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "averages" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 6, 85 | "id": "ff90bdad-135e-4e84-a176-c1171a2ee84a", 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": [ 91 | "{'current_price': 224.72,\n", 92 | " 'conservative_target': 230.4347,\n", 93 | " 'aggressive_target': 233.9653,\n", 94 | " 'average_price': 228.0563}" 95 | ] 96 | }, 97 | "execution_count": 6, 98 | "metadata": {}, 99 | "output_type": "execute_result" 100 | } 101 | ], 102 | "source": [ 103 | "price_target" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 7, 109 | "id": "94a14e75-3517-4606-84ff-2cb270da55ce", 110 | "metadata": {}, 111 | "outputs": [ 112 | { 113 | "data": { 114 | "text/plain": [ 115 | "{'avg_volume': 4548.0,\n", 116 | " 'latest_volume': 273,\n", 117 | " 'volume_change_percent': -93.99736147757255}" 118 | ] 119 | }, 120 | "execution_count": 7, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "volume_trend" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 8, 132 | "id": "b55ba173-759a-48de-82ea-336c9b40f99e", 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "{'rsi': 42.42,\n", 139 | " 'avg_gain': 0.8031,\n", 140 | " 'avg_loss': 1.0901,\n", 141 | " 'period': 14,\n", 142 | " 'days': 28}" 143 | ] 144 | }, 145 | "execution_count": 8, 146 | "metadata": {}, 147 | "output_type": "execute_result" 148 | } 149 | ], 150 | "source": [ 151 | "rsi" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "2950c286-0a81-4821-946a-c97af92e5d99", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "id": "e7a65b3a-10eb-4d72-816d-fb32fd392a4c", 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [] 169 | } 170 | ], 171 | "metadata": { 172 | "kernelspec": { 173 | "display_name": "Python 3 (ipykernel)", 174 | "language": "python", 175 | "name": "python3" 176 | }, 177 | "language_info": { 178 | "codemirror_mode": { 179 | "name": "ipython", 180 | "version": 3 181 | }, 182 | "file_extension": ".py", 183 | "mimetype": "text/x-python", 184 | "name": "python", 185 | "nbconvert_exporter": "python", 186 | "pygments_lexer": "ipython3", 187 | "version": "3.12.3" 188 | } 189 | }, 190 | "nbformat": 4, 191 | "nbformat_minor": 5 192 | } 193 | -------------------------------------------------------------------------------- /nbs/Decide - 1 - Logic-based Recommendation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market import services as market_services" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "b0d27cf2-6c7c-4dfa-9425-6da85d9a0e32", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "ticker = \"AAPL\"\n", 32 | "days = 30\n", 33 | "queryset = market_services.get_daily_stock_quotes_queryset(ticker, days=days)\n", 34 | "averages = market_services.get_daily_moving_averages(ticker, days=days, queryset=queryset)\n", 35 | "price_target = market_services.get_price_target(ticker, days=days, queryset=queryset)\n", 36 | "volume_trend = market_services.get_volume_trend(ticker, days=days, queryset=queryset)\n", 37 | "rsi_data = market_services.calculate_rsi(ticker, days=days, period=14)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 7, 43 | "id": "2950c286-0a81-4821-946a-c97af92e5d99", 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "data": { 48 | "text/plain": [ 49 | "{'current_price': 224.72,\n", 50 | " 'conservative_target': 230.4347,\n", 51 | " 'aggressive_target': 233.9653,\n", 52 | " 'average_price': 228.4458}" 53 | ] 54 | }, 55 | "execution_count": 7, 56 | "metadata": {}, 57 | "output_type": "execute_result" 58 | } 59 | ], 60 | "source": [ 61 | "price_target" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 5, 67 | "id": "e7a65b3a-10eb-4d72-816d-fb32fd392a4c", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "signals = []" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 6, 77 | "id": "12a20850-720a-4ea8-813a-8664b44f1d0a", 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "if averages.get('ma_5') > averages.get('ma_20'):\n", 82 | " signals.append(1)\n", 83 | "else:\n", 84 | " signals.append(-1)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 8, 90 | "id": "c18d7b23-06c2-44b7-96cc-6c4310d830bb", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "if price_target.get('current_price') < price_target.get('conservative_target'):\n", 95 | " signals.append(1)\n", 96 | "else:\n", 97 | " signals.append(-1)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 9, 103 | "id": "58291706-5bc2-4009-be15-f9548b7e1b28", 104 | "metadata": {}, 105 | "outputs": [ 106 | { 107 | "data": { 108 | "text/plain": [ 109 | "{'avg_volume': 4487.083333333333,\n", 110 | " 'latest_volume': 273,\n", 111 | " 'volume_change_percent': -93.91586962577769}" 112 | ] 113 | }, 114 | "execution_count": 9, 115 | "metadata": {}, 116 | "output_type": "execute_result" 117 | } 118 | ], 119 | "source": [ 120 | "volume_trend" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 10, 126 | "id": "8b3f8e11-c8d7-49ae-b73d-6be27a06a091", 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "if volume_trend.get(\"volume_change_percent\") > 20:\n", 131 | " signals.append(1)\n", 132 | "elif volume_trend.get(\"volume_change_percent\") < -20:\n", 133 | " signals.append(-1)\n", 134 | "else:\n", 135 | " signals.append(0)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 11, 141 | "id": "b96c970e-baa0-477e-bf3e-4c1b4dc9eb9a", 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "data": { 146 | "text/plain": [ 147 | "{'rsi': 39.6306,\n", 148 | " 'avg_gain': 0.7455,\n", 149 | " 'avg_loss': 1.1356,\n", 150 | " 'period': 14,\n", 151 | " 'days': 30}" 152 | ] 153 | }, 154 | "execution_count": 11, 155 | "metadata": {}, 156 | "output_type": "execute_result" 157 | } 158 | ], 159 | "source": [ 160 | "rsi_data" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 12, 166 | "id": "e77b6ada-ddab-4e6d-b2d8-41378b6677a3", 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "rsi = rsi_data.get('rsi')\n", 171 | "if rsi > 70:\n", 172 | " signals.append(-1) # Overbought\n", 173 | "elif rsi < 30:\n", 174 | " signals.append(1) # Oversold\n", 175 | "else:\n", 176 | " signals.append(0)" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 13, 182 | "id": "cd8d0220-ec15-4a25-9d51-764c41ae1f12", 183 | "metadata": {}, 184 | "outputs": [ 185 | { 186 | "data": { 187 | "text/plain": [ 188 | "-1" 189 | ] 190 | }, 191 | "execution_count": 13, 192 | "metadata": {}, 193 | "output_type": "execute_result" 194 | } 195 | ], 196 | "source": [ 197 | "score = sum(signals)\n", 198 | "score" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 19, 204 | "id": "7c6817f4-6676-4e2d-af52-4492e11bdc2a", 205 | "metadata": {}, 206 | "outputs": [ 207 | { 208 | "name": "stdout", 209 | "output_type": "stream", 210 | "text": [ 211 | "HOLD\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "if score>= 2:\n", 217 | " print(\"BUY\")\n", 218 | "elif score <= -2:\n", 219 | " print(\"SELL\")\n", 220 | "else:\n", 221 | " print(\"HOLD\")" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "id": "e0b30373-f882-41a7-a569-f5f543cfe7b4", 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "id": "b14531ce-5dfc-4620-96c7-bcab3d0dec45", 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [] 239 | } 240 | ], 241 | "metadata": { 242 | "kernelspec": { 243 | "display_name": "Python 3 (ipykernel)", 244 | "language": "python", 245 | "name": "python3" 246 | }, 247 | "language_info": { 248 | "codemirror_mode": { 249 | "name": "ipython", 250 | "version": 3 251 | }, 252 | "file_extension": ".py", 253 | "mimetype": "text/x-python", 254 | "name": "python", 255 | "nbconvert_exporter": "python", 256 | "pygments_lexer": "ipython3", 257 | "version": "3.12.3" 258 | } 259 | }, 260 | "nbformat": 4, 261 | "nbformat_minor": 5 262 | } 263 | -------------------------------------------------------------------------------- /nbs/Decide - 2 - LLM-based Recommendation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market import services as market_services" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "b0d27cf2-6c7c-4dfa-9425-6da85d9a0e32", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "id": "4b007fdc-9a97-4d82-b386-e1e885704073", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "def get_stock_indicators(ticker = \"AAPL\", days=30):\n", 40 | " queryset = market_services.get_daily_stock_quotes_queryset(ticker, days=days)\n", 41 | " if queryset.count() == 0:\n", 42 | " raise Exception(f\"Data for {ticker} not found\")\n", 43 | " averages = market_services.get_daily_moving_averages(ticker, days=days, queryset=queryset)\n", 44 | " price_target = market_services.get_price_target(ticker, days=days, queryset=queryset)\n", 45 | " volume_trend = market_services.get_volume_trend(ticker, days=days, queryset=queryset)\n", 46 | " rsi_data = market_services.calculate_rsi(ticker, days=days, period=14)\n", 47 | " signals = []\n", 48 | " if averages.get('ma_5') > averages.get('ma_20'):\n", 49 | " signals.append(1)\n", 50 | " else:\n", 51 | " signals.append(-1)\n", 52 | " if price_target.get('current_price') < price_target.get('conservative_target'):\n", 53 | " signals.append(1)\n", 54 | " else:\n", 55 | " signals.append(-1)\n", 56 | " if volume_trend.get(\"volume_change_percent\") > 20:\n", 57 | " signals.append(1)\n", 58 | " elif volume_trend.get(\"volume_change_percent\") < -20:\n", 59 | " signals.append(-1)\n", 60 | " else:\n", 61 | " signals.append(0)\n", 62 | " rsi = rsi_data.get('rsi')\n", 63 | " if rsi > 70:\n", 64 | " signals.append(-1) # Overbought\n", 65 | " elif rsi < 30:\n", 66 | " signals.append(1) # Oversold\n", 67 | " else:\n", 68 | " signals.append(0)\n", 69 | " return {\n", 70 | " \"score\": sum(signals),\n", 71 | " \"ticker\": ticker,\n", 72 | " \"indicators\": {\n", 73 | " **averages,\n", 74 | " **price_target,\n", 75 | " **volume_trend,\n", 76 | " **rsi_data,\n", 77 | " }\n", 78 | " \n", 79 | " }" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 4, 85 | "id": "cd8d0220-ec15-4a25-9d51-764c41ae1f12", 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": [ 91 | "{'score': 1,\n", 92 | " 'ticker': 'AAPL',\n", 93 | " 'indicators': {'ma_5': 225.55,\n", 94 | " 'ma_20': 227.6489,\n", 95 | " 'current_price': 227.5099,\n", 96 | " 'conservative_target': 233.2246,\n", 97 | " 'aggressive_target': 236.7552,\n", 98 | " 'average_price': 228.562,\n", 99 | " 'avg_volume': 8804.291666666666,\n", 100 | " 'latest_volume': 103886,\n", 101 | " 'volume_change_percent': 1079.947279499108,\n", 102 | " 'rsi': 42.676,\n", 103 | " 'avg_gain': 0.8386,\n", 104 | " 'avg_loss': 1.1265,\n", 105 | " 'period': 14,\n", 106 | " 'days': 30}}" 107 | ] 108 | }, 109 | "execution_count": 4, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "results = get_stock_indicators(ticker='AAPL')\n", 116 | "score = results.get(\"score\")\n", 117 | "results" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 7, 123 | "id": "17a821e5-1584-47f7-b53b-c2d204deabfe", 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "import json\n", 128 | "from decouple import config" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 8, 134 | "id": "7e5ec9bd-786c-405c-abe3-157889edefe4", 135 | "metadata": {}, 136 | "outputs": [ 137 | { 138 | "data": { 139 | "text/plain": [ 140 | "'{\"score\": 1, \"ticker\": \"AAPL\", \"indicators\": {\"ma_5\": 225.55, \"ma_20\": 227.6489, \"current_price\": 227.5099, \"conservative_target\": 233.2246, \"aggressive_target\": 236.7552, \"average_price\": 228.562, \"avg_volume\": 8804.291666666666, \"latest_volume\": 103886, \"volume_change_percent\": 1079.947279499108, \"rsi\": 42.676, \"avg_gain\": 0.8386, \"avg_loss\": 1.1265, \"period\": 14, \"days\": 30}}'" 141 | ] 142 | }, 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "output_type": "execute_result" 146 | } 147 | ], 148 | "source": [ 149 | "results_as_json = json.dumps(results)\n", 150 | "results_as_json" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 9, 156 | "id": "7f7cb784-4c04-4c3c-a37e-75b54be43388", 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "OPENAI_API_KEY=config(\"OPENAI_API_KEY\", default=None)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 10, 166 | "id": "879c180e-8fbb-47ca-a7e6-cd6e56c0639c", 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "assert OPENAI_API_KEY is not None" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 11, 176 | "id": "604bbae2-876e-4e94-8db4-9f070d2e6082", 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "from openai import OpenAI\n", 181 | "client = OpenAI(api_key=OPENAI_API_KEY)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 20, 187 | "id": "7c6817f4-6676-4e2d-af52-4492e11bdc2a", 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "response = client.chat.completions.create(\n", 192 | " model=\"gpt-4o-mini\",\n", 193 | " messages=[\n", 194 | " {\"role\": \"system\", \"content\": \"You are an expert an analyzing stocks and respond in JSON data\"},\n", 195 | " {\"role\": \"user\", \"content\": f\"Considering these results {results_as_json}, provide a recommendation\"}\n", 196 | " ],\n", 197 | " response_format={\n", 198 | " \"type\": \"json_schema\",\n", 199 | " \"json_schema\": {\n", 200 | " \"name\": \"recommendation\",\n", 201 | " \"schema\": {\n", 202 | " \"type\": \"object\",\n", 203 | " \"properties\": {\n", 204 | " \"buy\": {\n", 205 | " \"description\": \"Recommend to buy stock\",\n", 206 | " \"type\": \"boolean\"\n", 207 | " },\n", 208 | " \"sell\": {\n", 209 | " \"description\": \"Recommend to sell stock\",\n", 210 | " \"type\": \"boolean\"\n", 211 | " },\n", 212 | " \"hold\": {\n", 213 | " \"description\": \"Recommend to hold stock\",\n", 214 | " \"type\": \"boolean\"\n", 215 | " },\n", 216 | " \"explanation\": {\n", 217 | " \"description\": \"Explanation of reasoning in 1 or 2 sentences\",\n", 218 | " \"type\": \"string\"\n", 219 | " },\n", 220 | " \"additionalProperties\": False\n", 221 | " }\n", 222 | " }\n", 223 | " }\n", 224 | " }\n", 225 | ")" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 21, 231 | "id": "e0b30373-f882-41a7-a569-f5f543cfe7b4", 232 | "metadata": {}, 233 | "outputs": [ 234 | { 235 | "data": { 236 | "text/plain": [ 237 | "{'buy': False,\n", 238 | " 'sell': False,\n", 239 | " 'hold': True,\n", 240 | " 'explanation': 'The stock price of AAPL is currently below its moving averages and the RSI indicates it is close to oversold conditions. Although there is potential for a price increase towards the conservative and aggressive targets, the overall momentum suggests holding rather than buying or selling at this time.'}" 241 | ] 242 | }, 243 | "execution_count": 21, 244 | "metadata": {}, 245 | "output_type": "execute_result" 246 | } 247 | ], 248 | "source": [ 249 | "result = json.loads(response.choices[0].message.content)\n", 250 | "result" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 15, 256 | "id": "b14531ce-5dfc-4620-96c7-bcab3d0dec45", 257 | "metadata": {}, 258 | "outputs": [ 259 | { 260 | "data": { 261 | "text/plain": [ 262 | "True" 263 | ] 264 | }, 265 | "execution_count": 15, 266 | "metadata": {}, 267 | "output_type": "execute_result" 268 | } 269 | ], 270 | "source": [ 271 | "result.get('hold') is True" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "id": "3c65d0c6-3fd2-4315-850d-cdc1c6b8b835", 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [] 281 | } 282 | ], 283 | "metadata": { 284 | "kernelspec": { 285 | "display_name": "Python 3 (ipykernel)", 286 | "language": "python", 287 | "name": "python3" 288 | }, 289 | "language_info": { 290 | "codemirror_mode": { 291 | "name": "ipython", 292 | "version": 3 293 | }, 294 | "file_extension": ".py", 295 | "mimetype": "text/x-python", 296 | "name": "python", 297 | "nbconvert_exporter": "python", 298 | "pygments_lexer": "ipython3", 299 | "version": "3.12.3" 300 | } 301 | }, 302 | "nbformat": 4, 303 | "nbformat_minor": 5 304 | } 305 | -------------------------------------------------------------------------------- /nbs/Putting it all together.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e85ed483-d2ef-4920-a1b5-51dd0eddf4a6", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import setup\n", 11 | "setup.init_django()" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "9b9ca555-32fb-4f46-a606-48e409b235f7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from market import services as market_services\n", 22 | "from market import tasks as market_tasks\n", 23 | "from market.models import Company" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 3, 29 | "id": "17a821e5-1584-47f7-b53b-c2d204deabfe", 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "import json\n", 34 | "from decouple import config" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "id": "b0d27cf2-6c7c-4dfa-9425-6da85d9a0e32", 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "id": "4b007fdc-9a97-4d82-b386-e1e885704073", 49 | "metadata": {}, 50 | "outputs": [ 51 | { 52 | "data": { 53 | "text/plain": [ 54 | "1" 55 | ] 56 | }, 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "output_type": "execute_result" 60 | } 61 | ], 62 | "source": [ 63 | "ticker = \"AAPL\"\n", 64 | "name = \"Apple\"\n", 65 | "company, _ = Company.objects.get_or_create(name=name, ticker=ticker)\n", 66 | "company.id" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 6, 72 | "id": "5248e7f5-eb8b-4c41-bd1a-6b0998ec7267", 73 | "metadata": {}, 74 | "outputs": [ 75 | { 76 | "name": "stdout", 77 | "output_type": "stream", 78 | "text": [ 79 | "Historical sync days ago 30\n", 80 | "dataset length 4197\n", 81 | "Doing chunk 0\n", 82 | "finished chunk 0\n", 83 | "Doing chunk 1000\n", 84 | "finished chunk 1000\n", 85 | "Doing chunk 2000\n", 86 | "finished chunk 2000\n", 87 | "Doing chunk 3000\n", 88 | "finished chunk 3000\n", 89 | "Doing chunk 4000\n", 90 | "finished chunk 4000\n", 91 | "30 done\n", 92 | "\n", 93 | "Historical sync days ago 60\n", 94 | "dataset length 8043\n", 95 | "Doing chunk 0\n", 96 | "finished chunk 0\n", 97 | "Doing chunk 1000\n", 98 | "finished chunk 1000\n", 99 | "Doing chunk 2000\n", 100 | "finished chunk 2000\n", 101 | "Doing chunk 3000\n", 102 | "finished chunk 3000\n", 103 | "Doing chunk 4000\n", 104 | "finished chunk 4000\n", 105 | "Doing chunk 5000\n", 106 | "finished chunk 5000\n", 107 | "Doing chunk 6000\n", 108 | "finished chunk 6000\n", 109 | "Doing chunk 7000\n", 110 | "finished chunk 7000\n", 111 | "Doing chunk 8000\n", 112 | "finished chunk 8000\n", 113 | "60 done\n", 114 | "\n", 115 | "Historical sync days ago 90\n", 116 | "dataset length 11692\n", 117 | "Doing chunk 0\n", 118 | "finished chunk 0\n", 119 | "Doing chunk 1000\n", 120 | "finished chunk 1000\n", 121 | "Doing chunk 2000\n", 122 | "finished chunk 2000\n", 123 | "Doing chunk 3000\n", 124 | "finished chunk 3000\n", 125 | "Doing chunk 4000\n", 126 | "finished chunk 4000\n", 127 | "Doing chunk 5000\n", 128 | "finished chunk 5000\n", 129 | "Doing chunk 6000\n", 130 | "finished chunk 6000\n", 131 | "Doing chunk 7000\n", 132 | "finished chunk 7000\n", 133 | "Doing chunk 8000\n", 134 | "finished chunk 8000\n", 135 | "Doing chunk 9000\n", 136 | "finished chunk 9000\n", 137 | "Doing chunk 10000\n", 138 | "finished chunk 10000\n", 139 | "Doing chunk 11000\n", 140 | "finished chunk 11000\n", 141 | "90 done\n", 142 | "\n", 143 | "Historical sync days ago 120\n", 144 | "dataset length 12422\n", 145 | "Doing chunk 0\n", 146 | "finished chunk 0\n", 147 | "Doing chunk 1000\n", 148 | "finished chunk 1000\n", 149 | "Doing chunk 2000\n", 150 | "finished chunk 2000\n", 151 | "Doing chunk 3000\n", 152 | "finished chunk 3000\n", 153 | "Doing chunk 4000\n", 154 | "finished chunk 4000\n", 155 | "Doing chunk 5000\n", 156 | "finished chunk 5000\n", 157 | "Doing chunk 6000\n", 158 | "finished chunk 6000\n", 159 | "Doing chunk 7000\n", 160 | "finished chunk 7000\n", 161 | "Doing chunk 8000\n", 162 | "finished chunk 8000\n", 163 | "Doing chunk 9000\n", 164 | "finished chunk 9000\n", 165 | "Doing chunk 10000\n", 166 | "finished chunk 10000\n", 167 | "Doing chunk 11000\n", 168 | "finished chunk 11000\n", 169 | "Doing chunk 12000\n", 170 | "finished chunk 12000\n", 171 | "120 done\n", 172 | "\n", 173 | "Historical sync days ago 150\n", 174 | "dataset length 12264\n", 175 | "Doing chunk 0\n", 176 | "finished chunk 0\n", 177 | "Doing chunk 1000\n", 178 | "finished chunk 1000\n", 179 | "Doing chunk 2000\n", 180 | "finished chunk 2000\n", 181 | "Doing chunk 3000\n", 182 | "finished chunk 3000\n", 183 | "Doing chunk 4000\n", 184 | "finished chunk 4000\n", 185 | "Doing chunk 5000\n", 186 | "finished chunk 5000\n", 187 | "Doing chunk 6000\n", 188 | "finished chunk 6000\n", 189 | "Doing chunk 7000\n", 190 | "finished chunk 7000\n", 191 | "Doing chunk 8000\n", 192 | "finished chunk 8000\n", 193 | "Doing chunk 9000\n", 194 | "finished chunk 9000\n", 195 | "Doing chunk 10000\n", 196 | "finished chunk 10000\n", 197 | "Doing chunk 11000\n", 198 | "finished chunk 11000\n", 199 | "Doing chunk 12000\n", 200 | "finished chunk 12000\n", 201 | "150 done\n", 202 | "\n", 203 | "Historical sync days ago 180\n", 204 | "dataset length 12131\n", 205 | "Doing chunk 0\n", 206 | "finished chunk 0\n", 207 | "Doing chunk 1000\n", 208 | "finished chunk 1000\n", 209 | "Doing chunk 2000\n", 210 | "finished chunk 2000\n", 211 | "Doing chunk 3000\n", 212 | "finished chunk 3000\n", 213 | "Doing chunk 4000\n", 214 | "finished chunk 4000\n", 215 | "Doing chunk 5000\n", 216 | "finished chunk 5000\n", 217 | "Doing chunk 6000\n", 218 | "finished chunk 6000\n", 219 | "Doing chunk 7000\n", 220 | "finished chunk 7000\n", 221 | "Doing chunk 8000\n", 222 | "finished chunk 8000\n", 223 | "Doing chunk 9000\n", 224 | "finished chunk 9000\n", 225 | "Doing chunk 10000\n", 226 | "finished chunk 10000\n", 227 | "Doing chunk 11000\n", 228 | "finished chunk 11000\n", 229 | "Doing chunk 12000\n", 230 | "finished chunk 12000\n", 231 | "180 done\n", 232 | "\n", 233 | "Historical sync days ago 210\n", 234 | "dataset length 12157\n", 235 | "Doing chunk 0\n", 236 | "finished chunk 0\n", 237 | "Doing chunk 1000\n", 238 | "finished chunk 1000\n", 239 | "Doing chunk 2000\n", 240 | "finished chunk 2000\n", 241 | "Doing chunk 3000\n", 242 | "finished chunk 3000\n", 243 | "Doing chunk 4000\n", 244 | "finished chunk 4000\n", 245 | "Doing chunk 5000\n", 246 | "finished chunk 5000\n", 247 | "Doing chunk 6000\n", 248 | "finished chunk 6000\n", 249 | "Doing chunk 7000\n", 250 | "finished chunk 7000\n", 251 | "Doing chunk 8000\n", 252 | "finished chunk 8000\n", 253 | "Doing chunk 9000\n", 254 | "finished chunk 9000\n", 255 | "Doing chunk 10000\n", 256 | "finished chunk 10000\n", 257 | "Doing chunk 11000\n", 258 | "finished chunk 11000\n", 259 | "Doing chunk 12000\n", 260 | "finished chunk 12000\n", 261 | "210 done\n", 262 | "\n", 263 | "Historical sync days ago 240\n", 264 | "dataset length 12176\n", 265 | "Doing chunk 0\n", 266 | "finished chunk 0\n", 267 | "Doing chunk 1000\n", 268 | "finished chunk 1000\n", 269 | "Doing chunk 2000\n", 270 | "finished chunk 2000\n", 271 | "Doing chunk 3000\n", 272 | "finished chunk 3000\n", 273 | "Doing chunk 4000\n", 274 | "finished chunk 4000\n", 275 | "Doing chunk 5000\n", 276 | "finished chunk 5000\n", 277 | "Doing chunk 6000\n", 278 | "finished chunk 6000\n", 279 | "Doing chunk 7000\n", 280 | "finished chunk 7000\n", 281 | "Doing chunk 8000\n", 282 | "finished chunk 8000\n", 283 | "Doing chunk 9000\n", 284 | "finished chunk 9000\n", 285 | "Doing chunk 10000\n", 286 | "finished chunk 10000\n", 287 | "Doing chunk 11000\n", 288 | "finished chunk 11000\n", 289 | "Doing chunk 12000\n", 290 | "finished chunk 12000\n", 291 | "240 done\n", 292 | "\n", 293 | "Historical sync days ago 270\n", 294 | "dataset length 12176\n", 295 | "Doing chunk 0\n", 296 | "finished chunk 0\n", 297 | "Doing chunk 1000\n", 298 | "finished chunk 1000\n", 299 | "Doing chunk 2000\n", 300 | "finished chunk 2000\n", 301 | "Doing chunk 3000\n", 302 | "finished chunk 3000\n", 303 | "Doing chunk 4000\n", 304 | "finished chunk 4000\n", 305 | "Doing chunk 5000\n", 306 | "finished chunk 5000\n", 307 | "Doing chunk 6000\n", 308 | "finished chunk 6000\n", 309 | "Doing chunk 7000\n", 310 | "finished chunk 7000\n", 311 | "Doing chunk 8000\n", 312 | "finished chunk 8000\n", 313 | "Doing chunk 9000\n", 314 | "finished chunk 9000\n", 315 | "Doing chunk 10000\n", 316 | "finished chunk 10000\n", 317 | "Doing chunk 11000\n", 318 | "finished chunk 11000\n", 319 | "Doing chunk 12000\n", 320 | "finished chunk 12000\n", 321 | "270 done\n", 322 | "\n", 323 | "Historical sync days ago 300\n", 324 | "dataset length 12141\n", 325 | "Doing chunk 0\n", 326 | "finished chunk 0\n", 327 | "Doing chunk 1000\n", 328 | "finished chunk 1000\n", 329 | "Doing chunk 2000\n", 330 | "finished chunk 2000\n", 331 | "Doing chunk 3000\n", 332 | "finished chunk 3000\n", 333 | "Doing chunk 4000\n", 334 | "finished chunk 4000\n", 335 | "Doing chunk 5000\n", 336 | "finished chunk 5000\n", 337 | "Doing chunk 6000\n", 338 | "finished chunk 6000\n", 339 | "Doing chunk 7000\n", 340 | "finished chunk 7000\n", 341 | "Doing chunk 8000\n", 342 | "finished chunk 8000\n", 343 | "Doing chunk 9000\n", 344 | "finished chunk 9000\n", 345 | "Doing chunk 10000\n", 346 | "finished chunk 10000\n", 347 | "Doing chunk 11000\n", 348 | "finished chunk 11000\n", 349 | "Doing chunk 12000\n", 350 | "finished chunk 12000\n", 351 | "300 done\n", 352 | "\n", 353 | "Historical sync days ago 330\n", 354 | "dataset length 12171\n", 355 | "Doing chunk 0\n", 356 | "finished chunk 0\n", 357 | "Doing chunk 1000\n", 358 | "finished chunk 1000\n", 359 | "Doing chunk 2000\n", 360 | "finished chunk 2000\n", 361 | "Doing chunk 3000\n", 362 | "finished chunk 3000\n", 363 | "Doing chunk 4000\n", 364 | "finished chunk 4000\n", 365 | "Doing chunk 5000\n", 366 | "finished chunk 5000\n", 367 | "Doing chunk 6000\n", 368 | "finished chunk 6000\n", 369 | "Doing chunk 7000\n", 370 | "finished chunk 7000\n", 371 | "Doing chunk 8000\n", 372 | "finished chunk 8000\n", 373 | "Doing chunk 9000\n", 374 | "finished chunk 9000\n", 375 | "Doing chunk 10000\n", 376 | "finished chunk 10000\n", 377 | "Doing chunk 11000\n", 378 | "finished chunk 11000\n", 379 | "Doing chunk 12000\n", 380 | "finished chunk 12000\n", 381 | "330 done\n", 382 | "\n" 383 | ] 384 | } 385 | ], 386 | "source": [ 387 | "market_tasks.sync_historical_stock_data(years_ago=1, company_ids=[company.id], use_celery=False, verbose=True)\n", 388 | "\n", 389 | "# use celery / async\n", 390 | "# market_tasks.sync_historical_stock_data.delay(years_ago=5, company_ids=[company.id], use_celery=True, verbose=False)" 391 | ] 392 | }, 393 | { 394 | "cell_type": "code", 395 | "execution_count": 18, 396 | "id": "cd8d0220-ec15-4a25-9d51-764c41ae1f12", 397 | "metadata": {}, 398 | "outputs": [ 399 | { 400 | "data": { 401 | "text/plain": [ 402 | "{'score': -1,\n", 403 | " 'ticker': 'AAPL',\n", 404 | " 'indicators': {'ma_5': 225.598,\n", 405 | " 'ma_20': 227.6609,\n", 406 | " 'current_price': 227.75,\n", 407 | " 'conservative_target': 235.5237,\n", 408 | " 'aggressive_target': 240.3263,\n", 409 | " 'average_price': 226.4995,\n", 410 | " 'avg_volume': 4692.446153846154,\n", 411 | " 'latest_volume': 341,\n", 412 | " 'volume_change_percent': -92.7330013212725,\n", 413 | " 'rsi': 48.8471,\n", 414 | " 'avg_gain': 0.9153,\n", 415 | " 'avg_loss': 0.9585,\n", 416 | " 'period': 14,\n", 417 | " 'days': 90}}" 418 | ] 419 | }, 420 | "execution_count": 18, 421 | "metadata": {}, 422 | "output_type": "execute_result" 423 | } 424 | ], 425 | "source": [ 426 | "results = market_services.get_stock_indicators(ticker=ticker, days=90)\n", 427 | "results" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": 8, 433 | "id": "7e5ec9bd-786c-405c-abe3-157889edefe4", 434 | "metadata": {}, 435 | "outputs": [ 436 | { 437 | "data": { 438 | "text/plain": [ 439 | "'{\"score\": -1, \"ticker\": \"AAPL\", \"indicators\": {\"ma_5\": 225.598, \"ma_20\": 227.6609, \"current_price\": 227.75, \"conservative_target\": 233.4647, \"aggressive_target\": 236.9953, \"average_price\": 228.572, \"avg_volume\": 4489.916666666667, \"latest_volume\": 341, \"volume_change_percent\": -92.4052042539765, \"rsi\": 42.9317, \"avg_gain\": 0.8474, \"avg_loss\": 1.1265, \"period\": 14, \"days\": 30}}'" 440 | ] 441 | }, 442 | "execution_count": 8, 443 | "metadata": {}, 444 | "output_type": "execute_result" 445 | } 446 | ], 447 | "source": [ 448 | "results_as_json = json.dumps(results)\n", 449 | "results_as_json" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 9, 455 | "id": "7f7cb784-4c04-4c3c-a37e-75b54be43388", 456 | "metadata": {}, 457 | "outputs": [], 458 | "source": [ 459 | "OPENAI_API_KEY=config(\"OPENAI_API_KEY\", default=None)" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 10, 465 | "id": "879c180e-8fbb-47ca-a7e6-cd6e56c0639c", 466 | "metadata": {}, 467 | "outputs": [], 468 | "source": [ 469 | "assert OPENAI_API_KEY is not None" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 11, 475 | "id": "604bbae2-876e-4e94-8db4-9f070d2e6082", 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "from openai import OpenAI\n", 480 | "client = OpenAI(api_key=OPENAI_API_KEY)" 481 | ] 482 | }, 483 | { 484 | "cell_type": "code", 485 | "execution_count": 15, 486 | "id": "7c6817f4-6676-4e2d-af52-4492e11bdc2a", 487 | "metadata": {}, 488 | "outputs": [], 489 | "source": [ 490 | "response = client.chat.completions.create(\n", 491 | " model=\"gpt-4o-mini\",\n", 492 | " messages=[\n", 493 | " {\"role\": \"system\", \"content\": \"You are an expert an analyzing stocks and respond in JSON data\"},\n", 494 | " {\"role\": \"user\", \"content\": f\"Considering these results {results_as_json}, provide a recommendation\"}\n", 495 | " ],\n", 496 | " response_format={\n", 497 | " \"type\": \"json_schema\",\n", 498 | " \"json_schema\": {\n", 499 | " \"name\": \"recommendation\",\n", 500 | " \"schema\": {\n", 501 | " \"type\": \"object\",\n", 502 | " \"properties\": {\n", 503 | " \"buy\": {\n", 504 | " \"description\": \"Recommend to buy stock\",\n", 505 | " \"type\": \"boolean\"\n", 506 | " },\n", 507 | " \"sell\": {\n", 508 | " \"description\": \"Recommend to sell stock\",\n", 509 | " \"type\": \"boolean\"\n", 510 | " },\n", 511 | " \"hold\": {\n", 512 | " \"description\": \"Recommend to hold stock\",\n", 513 | " \"type\": \"boolean\"\n", 514 | " },\n", 515 | " \"explanation\": {\n", 516 | " \"description\": \"Explanation of reasoning in 1 or 2 sentences\",\n", 517 | " \"type\": \"string\"\n", 518 | " },\n", 519 | " \"additionalProperties\": False\n", 520 | " }\n", 521 | " }\n", 522 | " }\n", 523 | " }\n", 524 | ")" 525 | ] 526 | }, 527 | { 528 | "cell_type": "code", 529 | "execution_count": 16, 530 | "id": "e0b30373-f882-41a7-a569-f5f543cfe7b4", 531 | "metadata": {}, 532 | "outputs": [ 533 | { 534 | "data": { 535 | "text/plain": [ 536 | "{'buy': False,\n", 537 | " 'sell': True,\n", 538 | " 'hold': False,\n", 539 | " 'explanation': 'Given the negative score and significant drop in volume along with an RSI of 42, which indicates potential weakness, it is advisable to sell the stock.'}" 540 | ] 541 | }, 542 | "execution_count": 16, 543 | "metadata": {}, 544 | "output_type": "execute_result" 545 | } 546 | ], 547 | "source": [ 548 | "result = json.loads(response.choices[0].message.content)\n", 549 | "result" 550 | ] 551 | }, 552 | { 553 | "cell_type": "code", 554 | "execution_count": 17, 555 | "id": "b14531ce-5dfc-4620-96c7-bcab3d0dec45", 556 | "metadata": {}, 557 | "outputs": [ 558 | { 559 | "data": { 560 | "text/plain": [ 561 | "False" 562 | ] 563 | }, 564 | "execution_count": 17, 565 | "metadata": {}, 566 | "output_type": "execute_result" 567 | } 568 | ], 569 | "source": [ 570 | "result.get('hold') is True" 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": null, 576 | "id": "3c65d0c6-3fd2-4315-850d-cdc1c6b8b835", 577 | "metadata": {}, 578 | "outputs": [], 579 | "source": [] 580 | } 581 | ], 582 | "metadata": { 583 | "kernelspec": { 584 | "display_name": "Python 3 (ipykernel)", 585 | "language": "python", 586 | "name": "python3" 587 | }, 588 | "language_info": { 589 | "codemirror_mode": { 590 | "name": "ipython", 591 | "version": 3 592 | }, 593 | "file_extension": ".py", 594 | "mimetype": "text/x-python", 595 | "name": "python", 596 | "nbconvert_exporter": "python", 597 | "pygments_lexer": "ipython3", 598 | "version": "3.12.3" 599 | } 600 | }, 601 | "nbformat": 4, 602 | "nbformat_minor": 5 603 | } 604 | -------------------------------------------------------------------------------- /nbs/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pathlib 4 | 5 | THIS_FILE_PATH = pathlib.Path(__file__).resolve() 6 | NBS_DIR = THIS_FILE_PATH.parent 7 | REPO_DIR = NBS_DIR.parent 8 | DJANGO_BASE_DIR = REPO_DIR / "src" 9 | 10 | 11 | def init_django(project_name='cfehome'): 12 | """Run administrative tasks.""" 13 | os.chdir(DJANGO_BASE_DIR) 14 | sys.path.insert(0, str(DJANGO_BASE_DIR)) 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings") 16 | os.environ['DJANGO_ALLOW_ASYNC_UNSAFE'] = "true" 17 | import django 18 | django.setup() 19 | 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5.1,<5.2 2 | django-timescaledb 3 | dj-database-url 4 | jupyter 5 | requests 6 | psycopg[binary] 7 | python-decouple 8 | pytz 9 | Celery 10 | Redis 11 | django-celery-beat 12 | django-celery-results 13 | django-admin-interface 14 | django-admin-rangefilter 15 | openai -------------------------------------------------------------------------------- /src/cfehome/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = [ 4 | "celery_app" 5 | ] -------------------------------------------------------------------------------- /src/cfehome/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for cfehome project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cfehome.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /src/cfehome/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cfehome.settings") 5 | 6 | app = Celery("cfehome") 7 | app.config_from_object( 8 | 'django.conf:settings', 9 | namespace='CELERY' 10 | ) 11 | app.autodiscover_tasks() 12 | -------------------------------------------------------------------------------- /src/cfehome/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cfehome project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from decouple import config 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "django-insecure-by#99_5yq34k3sxulo3j4r_f_4aube=$a7okpi^musps8dv+f7" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'admin_interface', 36 | 'colorfield', 37 | "django.contrib.admin", 38 | "django.contrib.auth", 39 | "django.contrib.contenttypes", 40 | "django.contrib.sessions", 41 | "django.contrib.messages", 42 | "django.contrib.staticfiles", 43 | "market", 44 | 'django_celery_beat', 45 | 'django_celery_results', 46 | 'rangefilter' 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | "django.middleware.security.SecurityMiddleware", 51 | "django.contrib.sessions.middleware.SessionMiddleware", 52 | "django.middleware.common.CommonMiddleware", 53 | "django.middleware.csrf.CsrfViewMiddleware", 54 | "django.contrib.auth.middleware.AuthenticationMiddleware", 55 | "django.contrib.messages.middleware.MessageMiddleware", 56 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 57 | ] 58 | 59 | ROOT_URLCONF = "cfehome.urls" 60 | 61 | TEMPLATES = [ 62 | { 63 | "BACKEND": "django.template.backends.django.DjangoTemplates", 64 | "DIRS": [], 65 | "APP_DIRS": True, 66 | "OPTIONS": { 67 | "context_processors": [ 68 | "django.template.context_processors.debug", 69 | "django.template.context_processors.request", 70 | "django.contrib.auth.context_processors.auth", 71 | "django.contrib.messages.context_processors.messages", 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = "cfehome.wsgi.application" 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | "default": { 85 | "ENGINE": "django.db.backends.sqlite3", 86 | "NAME": BASE_DIR / "db.sqlite3", 87 | } 88 | } 89 | 90 | DATABASE_URL = config("DATABASE_URL", default="", cast=str) 91 | 92 | if DATABASE_URL != "": 93 | import dj_database_url 94 | DATABASES = { 95 | "default": dj_database_url.config( 96 | default=DATABASE_URL, 97 | conn_max_age=300, 98 | engine='timescale.db.backends.postgresql', 99 | ) 100 | } 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 108 | }, 109 | { 110 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 111 | }, 112 | { 113 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 114 | }, 115 | { 116 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 117 | }, 118 | ] 119 | 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 123 | 124 | LANGUAGE_CODE = "en-us" 125 | 126 | TIME_ZONE = "UTC" 127 | 128 | USE_I18N = True 129 | 130 | USE_TZ = True 131 | 132 | 133 | # Static files (CSS, JavaScript, Images) 134 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 135 | 136 | STATIC_URL = "static/" 137 | 138 | # Default primary key field type 139 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 140 | 141 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 142 | 143 | REDIS_URL = config("REDIS_URL", default='redis://localhost:6379') 144 | 145 | CELERY_BROKER_URL = REDIS_URL 146 | CELERY_RESULT_BACKEND = "django-db" 147 | CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" 148 | 149 | 150 | CELERY_BROKER_CONNECTION_RETRY = True 151 | CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True 152 | CELERY_REDIS_BACKEND_USE_SSL = False 153 | CELERY_BROKER_USE_SSL = False -------------------------------------------------------------------------------- /src/cfehome/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for cfehome project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path("admin/", admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /src/cfehome/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cfehome project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cfehome.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Stock-Trading-Bot/13c2d66e06df7f0209b50756865d773aeb49c010/src/helpers/__init__.py -------------------------------------------------------------------------------- /src/helpers/clients/__init__.py: -------------------------------------------------------------------------------- 1 | from ._alpha_vantage import AlphaVantageAPIClient 2 | from ._polygon import PolygonAPIClient 3 | 4 | __all__ = [ 5 | "AlphaVantageAPIClient", 6 | "PolygonAPIClient" 7 | ] -------------------------------------------------------------------------------- /src/helpers/clients/_alpha_vantage.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import requests 3 | 4 | from decouple import config 5 | from dataclasses import dataclass 6 | from typing import Literal 7 | from urllib.parse import urlencode 8 | from datetime import datetime 9 | from decimal import Decimal 10 | 11 | ALPHA_VANTAGE_API_KEY = config("ALPHA_VANTAGE_API_KEY", default=None, cast=str) 12 | 13 | def transform_alpha_vantage_result(timestamp_str, result): 14 | # unix_timestamp = result.get('t') / 1000.0 15 | # utc_timestamp = datetime.fromtimestamp(unix_timestamp, tz=pytz.timezone('UTC')) 16 | timestamp_format = '%Y-%m-%d %H:%M:%S' 17 | eastern = pytz.timezone("US/Eastern") 18 | utc = pytz.utc 19 | timestamp = eastern.localize(datetime.strptime(timestamp_str,timestamp_format)).astimezone(utc) 20 | return { 21 | 'open_price': Decimal(result['1. open']), 22 | 'close_price': Decimal(result['4. close']), 23 | 'high_price': Decimal(result['2. high']), 24 | 'low_price': Decimal(result['3. low']), 25 | 'number_of_trades': None, 26 | 'volume': int(result['5. volume']), 27 | 'volume_weighted_average': None, 28 | 'raw_timestamp': timestamp_str, 29 | 'time': timestamp, 30 | } 31 | 32 | 33 | 34 | 35 | @dataclass 36 | class AlphaVantageAPIClient: 37 | ticker: str = "AAPL" 38 | function: Literal["TIME_SERIES_INTRADAY"] = "TIME_SERIES_INTRADAY" 39 | interval: Literal["1min", "5min", "15min", "30min", "60min"] = "1min" 40 | month: str = "2024-01" 41 | api_key: str = "" 42 | 43 | def get_api_key(self): 44 | return self.api_key or ALPHA_VANTAGE_API_KEY 45 | 46 | def get_headers(self): 47 | api_key = self.get_api_key() 48 | return {} 49 | 50 | def get_params(self): 51 | return { 52 | "apikey": self.get_api_key(), 53 | "symbol": self.ticker, 54 | "interval": self.interval, 55 | "function": self.function, 56 | "month": self.month, 57 | 58 | } 59 | 60 | def generate_url(self, pass_auth=False): 61 | path = "/query" 62 | url = f"https://www.alphavantage.co{path}" 63 | params = self.get_params() 64 | encoded_params = urlencode(params) 65 | url = f"{url}?{encoded_params}" 66 | if pass_auth: 67 | api_key = self.get_api_key() 68 | url += f"&api_key={api_key}" 69 | return url 70 | 71 | def fetch_data(self): 72 | headers = self.get_headers() 73 | url = self.generate_url() 74 | response = requests.get(url, headers=headers) 75 | response.raise_for_status() # not 200/201 76 | return response.json() 77 | 78 | def get_stock_data(self): 79 | data = self.fetch_data() 80 | dataset_key = [x for x in list(data.keys()) if not x.lower() == "meta data"][0] 81 | results = data[dataset_key] 82 | dataset = [] 83 | for timestamp_str in results.keys(): 84 | dataset.append( 85 | transform_alpha_vantage_result(timestamp_str, results.get(timestamp_str)) 86 | ) 87 | return dataset 88 | -------------------------------------------------------------------------------- /src/helpers/clients/_polygon.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import requests 3 | 4 | from dataclasses import dataclass 5 | from typing import Literal 6 | from urllib.parse import urlencode 7 | from datetime import datetime 8 | from decouple import config 9 | 10 | POLOGYON_API_KEY = config("POLOGYON_API_KEY", default=None, cast=str) 11 | 12 | 13 | def transform_polygon_result(result): 14 | unix_timestamp = result.get('t') / 1000.0 15 | utc_timestamp = datetime.fromtimestamp(unix_timestamp, tz=pytz.timezone('UTC')) 16 | return { 17 | 'open_price': result['o'], 18 | 'close_price': result['c'], 19 | 'high_price': result['h'], 20 | 'low_price': result['l'], 21 | 'number_of_trades': result['n'], 22 | 'volume': result['v'], 23 | 'volume_weighted_average': result['vw'], 24 | 'raw_timestamp': result.get('t'), 25 | 'time': utc_timestamp, 26 | } 27 | 28 | 29 | @dataclass 30 | class PolygonAPIClient: 31 | ticker: str = "AAPL" 32 | multiplier: int = 5 33 | timespan:str = "minute" 34 | from_date:str = "2024-01-09" 35 | to_date:str = "2024-01-09" 36 | api_key:str = "" 37 | adjusted: bool = True 38 | sort: Literal["asc", "desc"] = "asc" 39 | 40 | def get_api_key(self): 41 | return self.api_key or POLOGYON_API_KEY 42 | 43 | def get_headers(self): 44 | api_key = self.get_api_key() 45 | return { 46 | "Authorization": f"Bearer {api_key}" 47 | } 48 | 49 | def get_params(self): 50 | return { 51 | "adjusted": self.adjusted, 52 | "sort": self.sort, 53 | "limit": 50_000, 54 | } 55 | 56 | def generate_url(self, pass_auth=False): 57 | ticker = f"{self.ticker}".upper() 58 | path = f"/v2/aggs/ticker/{ticker}/range/{self.multiplier}/{self.timespan}/{self.from_date}/{self.to_date}" 59 | url = f"https://api.polygon.io{path}" 60 | params = self.get_params() 61 | encoded_params = urlencode(params) 62 | url = f"{url}?{encoded_params}" 63 | if pass_auth: 64 | api_key = self.get_api_key() 65 | url += f"&api_key={api_key}" 66 | return url 67 | 68 | def fetch_data(self): 69 | headers = self.get_headers() 70 | url = self.generate_url() 71 | response = requests.get(url, headers=headers) 72 | response.raise_for_status() # not 200/201 73 | return response.json() 74 | 75 | def get_stock_data(self): 76 | data = self.fetch_data() 77 | results = data.get('results') or None 78 | if results is None: 79 | raise Exception(f"Ticker {self.ticker} has no results") 80 | dataset = [] 81 | for result in results: 82 | dataset.append( 83 | transform_polygon_result(result) 84 | ) 85 | return dataset -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cfehome.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /src/market/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Stock-Trading-Bot/13c2d66e06df7f0209b50756865d773aeb49c010/src/market/__init__.py -------------------------------------------------------------------------------- /src/market/admin.py: -------------------------------------------------------------------------------- 1 | import zoneinfo 2 | from django.contrib import admin 3 | from django.utils import timezone 4 | from rangefilter.filters import ( 5 | DateTimeRangeFilterBuilder, 6 | ) 7 | 8 | # Register your models here. 9 | from .models import StockQuote, Company 10 | 11 | admin.site.register(Company) 12 | 13 | class StockQuoteAdmin(admin.ModelAdmin): 14 | list_display = ['company__ticker', 'close_price', 'localized_time', 'time'] 15 | list_filter = [ 16 | 'company__ticker', 17 | ('time', DateTimeRangeFilterBuilder()), 18 | 'time' 19 | ] 20 | readonly_fields = ['localized_time', 'time','raw_timestamp'] 21 | 22 | def localized_time(self, obj): 23 | tz_name = "US/Eastern" 24 | user_tz = zoneinfo.ZoneInfo(tz_name) 25 | local_time = obj.time.astimezone(user_tz) 26 | return local_time.strftime("%b %d, %Y, %I:%M %p (%Z)") 27 | 28 | def get_queryset(self, request): 29 | tz_name = "US/Eastern" 30 | tz_name = "UTC" 31 | user_tz = zoneinfo.ZoneInfo(tz_name) 32 | timezone.activate(user_tz) 33 | return super().get_queryset(request) 34 | 35 | # class Meta: 36 | # model = StockQuote 37 | 38 | admin.site.register(StockQuote, StockQuoteAdmin) -------------------------------------------------------------------------------- /src/market/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MarketConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "market" 7 | -------------------------------------------------------------------------------- /src/market/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-07 23:28 2 | 3 | import django.db.models.deletion 4 | import django.db.models.manager 5 | import timescale.db.models.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Company", 17 | fields=[ 18 | ( 19 | "id", 20 | models.BigAutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("name", models.CharField(max_length=120)), 28 | ("ticker", models.CharField(db_index=True, max_length=20, unique=True)), 29 | ("description", models.TextField(blank=True, null=True)), 30 | ("active", models.BooleanField(default=True)), 31 | ("timestamp", models.DateTimeField(auto_now_add=True)), 32 | ("updated", models.DateTimeField(auto_now=True)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name="StockQuote", 37 | fields=[ 38 | ( 39 | "id", 40 | models.BigAutoField( 41 | auto_created=True, 42 | primary_key=True, 43 | serialize=False, 44 | verbose_name="ID", 45 | ), 46 | ), 47 | ("open_price", models.DecimalField(decimal_places=4, max_digits=10)), 48 | ("close_price", models.DecimalField(decimal_places=4, max_digits=10)), 49 | ("high_price", models.DecimalField(decimal_places=4, max_digits=10)), 50 | ("low_price", models.DecimalField(decimal_places=4, max_digits=10)), 51 | ("number_of_trades", models.BigIntegerField(blank=True, null=True)), 52 | ("volume", models.BigIntegerField()), 53 | ( 54 | "volume_weighted_average", 55 | models.DecimalField(decimal_places=4, max_digits=10), 56 | ), 57 | ( 58 | "time", 59 | timescale.db.models.fields.TimescaleDateTimeField( 60 | interval="1 week" 61 | ), 62 | ), 63 | ( 64 | "company", 65 | models.ForeignKey( 66 | on_delete=django.db.models.deletion.CASCADE, 67 | related_name="stock_quotes", 68 | to="market.company", 69 | ), 70 | ), 71 | ], 72 | managers=[ 73 | ("timescale", django.db.models.manager.Manager()), 74 | ], 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /src/market/migrations/0002_alter_stockquote_volume_weighted_average.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-07 23:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("market", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="stockquote", 14 | name="volume_weighted_average", 15 | field=models.DecimalField(decimal_places=6, max_digits=10), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/market/migrations/0003_alter_stockquote_managers.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-08 00:02 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("market", "0002_alter_stockquote_volume_weighted_average"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelManagers( 13 | name="stockquote", 14 | managers=[], 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /src/market/migrations/0004_alter_stockquote_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-08 00:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("market", "0003_alter_stockquote_managers"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="stockquote", 14 | unique_together={("company", "time")}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /src/market/migrations/0005_stockquote_raw_timestamp.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.3 on 2024-11-12 23:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("market", "0004_alter_stockquote_unique_together"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="stockquote", 14 | name="raw_timestamp", 15 | field=models.CharField(blank=True, max_length=120, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/market/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Stock-Trading-Bot/13c2d66e06df7f0209b50756865d773aeb49c010/src/market/migrations/__init__.py -------------------------------------------------------------------------------- /src/market/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from timescale.db.models.fields import TimescaleDateTimeField 4 | from timescale.db.models.managers import TimescaleManager 5 | 6 | from . import tasks 7 | # Create your models here. 8 | class Company(models.Model): 9 | name = models.CharField(max_length=120) 10 | ticker = models.CharField(max_length=20, unique=True, db_index=True) 11 | description = models.TextField(blank=True, null=True) 12 | active = models.BooleanField(default=True) 13 | timestamp = models.DateTimeField(auto_now_add=True) 14 | updated = models.DateTimeField(auto_now=True) 15 | 16 | def save(self, *args, **kwargs): 17 | self.ticker = f"{self.ticker}".upper() 18 | super().save(*args, **kwargs) 19 | tasks.sync_company_stock_quotes.delay(self.pk) 20 | 21 | class StockQuote(models.Model): 22 | """ 23 | 'open_price': 140.41, 24 | 'close_price': 140.41, 25 | 'high_price': 140.41, 26 | 'low_price': 140.41, 27 | 'number_of_trades': 3, 28 | 'volume': 134, 29 | 'volume_weighted_average': 140.3984, 30 | 'time': datetime.datetime(2024, 1, 9, 9, 2, tzinfo=) 31 | """ 32 | company = models.ForeignKey( 33 | Company, 34 | on_delete=models.CASCADE, 35 | related_name="stock_quotes" 36 | ) 37 | open_price = models.DecimalField(max_digits=10, decimal_places=4) 38 | close_price = models.DecimalField(max_digits=10, decimal_places=4) 39 | high_price = models.DecimalField(max_digits=10, decimal_places=4) 40 | low_price = models.DecimalField(max_digits=10, decimal_places=4) 41 | number_of_trades = models.BigIntegerField(blank=True, null=True) 42 | volume = models.BigIntegerField() 43 | volume_weighted_average = models.DecimalField(max_digits=10, decimal_places=6) 44 | raw_timestamp = models.CharField(max_length=120, null=True, blank=True, help_text="Non transformed timestamp string or int or float") 45 | time = TimescaleDateTimeField(interval="1 week") 46 | 47 | objects = models.Manager() 48 | timescale = TimescaleManager() 49 | 50 | class Meta: 51 | unique_together = [('company', 'time')] -------------------------------------------------------------------------------- /src/market/services.py: -------------------------------------------------------------------------------- 1 | from django.db.models import ( 2 | Avg, 3 | F, 4 | RowRange, 5 | Window, 6 | Max, 7 | Min, 8 | ExpressionWrapper, 9 | DecimalField, 10 | Case, 11 | When, 12 | Value 13 | ) 14 | from django.db.models.functions import TruncDate, FirstValue, Lag, Coalesce 15 | from django.utils import timezone 16 | from datetime import timedelta 17 | from decimal import Decimal 18 | 19 | 20 | from market.models import StockQuote 21 | 22 | 23 | def get_daily_stock_quotes_queryset(ticker, days=28, use_bucket=False): 24 | now = timezone.now() 25 | start_date = now - timedelta(days=days) 26 | end_date = now 27 | lastest_daily_timestamps = ( 28 | StockQuote.objects.filter(company__ticker=ticker, time__range=(start_date - timedelta(days=40), end_date)) 29 | .annotate(date=TruncDate('time')) 30 | .values('company', 'date') 31 | .annotate(latest_time=Max('time')) 32 | .values('company', 'date', 'latest_time') 33 | .order_by('date') 34 | ) 35 | acutal_timestamps = [x['latest_time'] for x in lastest_daily_timestamps] 36 | qs = StockQuote.timescale.filter( 37 | company__ticker=ticker, 38 | time__range=(start_date, end_date), 39 | time__in=acutal_timestamps 40 | ) 41 | if use_bucket: 42 | return qs.time_bucket('time', '1 day') 43 | return qs 44 | 45 | 46 | 47 | def get_daily_moving_averages(ticker, days=28, queryset=None): 48 | if queryset is None: 49 | queryset = get_daily_stock_quotes_queryset(ticker=ticker, days=days) 50 | obj = queryset.annotate( 51 | ma_5=Window( 52 | expression=Avg('close_price'), 53 | order_by=F('time').asc(), 54 | partition_by=[], 55 | frame=RowRange(start=-4, end=0), 56 | ), 57 | ma_20=Window( 58 | expression=Avg('close_price'), 59 | order_by=F('time').asc(), 60 | partition_by=[], 61 | frame=RowRange(start=-19, end=0), 62 | ) 63 | ).order_by('-time').first() 64 | if not obj: 65 | return None 66 | ma_5 = obj.ma_5 67 | ma_20 = obj.ma_20 68 | if ma_5 is None or ma_20 is None: 69 | return None 70 | if ma_5 <= 0 or ma_20 <= 0: 71 | return None 72 | return { 73 | "ma_5": float(round(ma_5, 4)), 74 | "ma_20": float(round(ma_20, 4)) 75 | } 76 | 77 | 78 | def get_price_target(ticker, days=28, queryset=None): 79 | """ 80 | Simplified price target calculation 81 | """ 82 | if queryset is None: 83 | queryset = get_daily_stock_quotes_queryset(ticker, days=days) 84 | daily_data = ( 85 | queryset 86 | .annotate( 87 | latest_price=Window( 88 | expression=FirstValue('close_price'), 89 | partition_by=[], 90 | order_by=F('time').desc() 91 | ) 92 | ) 93 | .aggregate( 94 | current_price=Max('latest_price'), 95 | avg_price=Avg('close_price'), 96 | highest=Max('high_price'), 97 | lowest=Min('low_price') 98 | ) 99 | ) 100 | 101 | if not daily_data: 102 | return None 103 | current_price = float(daily_data['current_price']) 104 | avg_price = float(daily_data['avg_price']) 105 | price_range = float(daily_data['highest']) - float(daily_data['lowest']) 106 | 107 | # Simple target based on average price and recent range 108 | conservative_target = current_price + (price_range * 0.382) # 38.2% Fibonacci 109 | aggressive_target = current_price + (price_range * 0.618) # 61.8% Fibonacci 110 | 111 | return { 112 | 'current_price': round(current_price, 4), 113 | 'conservative_target': round(conservative_target, 4), 114 | 'aggressive_target': round(aggressive_target, 4), 115 | 'average_price': round(avg_price, 4) 116 | } 117 | 118 | def get_volume_trend(ticker, days=28, queryset=None): 119 | """ 120 | Analyze recent volume trends 121 | """ 122 | if queryset is None: 123 | queryset = get_daily_stock_quotes_queryset(ticker=ticker, days=days) 124 | start = -(days - 1) 125 | data = queryset.annotate( 126 | avg_volume=Window( 127 | expression=Avg('volume'), 128 | order_by=F('time').asc(), 129 | partition_by=[], 130 | frame=RowRange(start=start, end=0) 131 | ) 132 | ).order_by('-time').first() 133 | 134 | if not data: 135 | return None 136 | vol = data.volume 137 | avg_vol = data.avg_volume 138 | volume_change = 0 139 | if vol is None or avg_vol is None: 140 | return None 141 | if vol > 0 and avg_vol > 0: 142 | volume_change = (( vol - avg_vol) / avg_vol) * 100 143 | return { 144 | 'avg_volume': float(avg_vol), 145 | 'latest_volume': int(vol), 146 | 'volume_change_percent': float(volume_change) 147 | } 148 | 149 | def calculate_rsi(ticker, days=28, queryset=None, period=14): 150 | """ 151 | Calculate Relative Strength Index (RSI) using Django ORM. 152 | 153 | Args: 154 | ticker (str): Stock ticker symbol 155 | days (int): Days in the price data (default: 28) 156 | queryset (list): Stock Quote querset 157 | period (int): RSI period (default: 14) 158 | 159 | Returns: 160 | dict: RSI value and component calculations 161 | """ 162 | # Get daily price data 163 | if period is None: 164 | period = int(days / 4) 165 | if queryset is None: 166 | queryset = get_daily_stock_quotes_queryset(ticker, days=days, use_bucket=True) 167 | 168 | 169 | # Calculate price changes and gains/losses with explicit decimal conversion 170 | movement = queryset.annotate( 171 | closing_price=ExpressionWrapper( 172 | F('close_price'), 173 | output_field=DecimalField(max_digits=10, decimal_places=4) 174 | ), 175 | prev_close=Window( 176 | expression=Lag('close_price'), 177 | order_by=F('bucket').asc(), 178 | partition_by=[], 179 | output_field=DecimalField(max_digits=10, decimal_places=4) 180 | ) 181 | ).annotate( 182 | price_change=ExpressionWrapper( 183 | F('close_price') - F('prev_close'), 184 | output_field=DecimalField(max_digits=10, decimal_places=4) 185 | ), 186 | gain=Case( 187 | When(price_change__gt=0, 188 | then=ExpressionWrapper( 189 | F('price_change'), 190 | output_field=DecimalField(max_digits=10, decimal_places=4) 191 | )), 192 | default=Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)), 193 | output_field=DecimalField(max_digits=10, decimal_places=4) 194 | ), 195 | loss=Case( 196 | When(price_change__lt=0, 197 | then=ExpressionWrapper( 198 | -F('price_change'), 199 | output_field=DecimalField(max_digits=10, decimal_places=4) 200 | )), 201 | default=Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)), 202 | output_field=DecimalField(max_digits=10, decimal_places=4) 203 | ) 204 | ) 205 | 206 | # Calculate initial averages for the first period 207 | initial_avg = movement.exclude(prev_close__isnull=True)[:period].aggregate( 208 | avg_gain=Coalesce( 209 | ExpressionWrapper( 210 | Avg('gain'), 211 | output_field=DecimalField(max_digits=10, decimal_places=4) 212 | ), 213 | Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)) 214 | ), 215 | avg_loss=Coalesce( 216 | ExpressionWrapper( 217 | Avg('loss'), 218 | output_field=DecimalField(max_digits=10, decimal_places=4) 219 | ), 220 | Value(0, output_field=DecimalField(max_digits=10, decimal_places=4)) 221 | ) 222 | ) 223 | 224 | # Get subsequent data points for EMA calculation 225 | subsequent_data = list(movement.exclude(prev_close__isnull=True)[period:].values('gain', 'loss')) 226 | 227 | # Calculate EMA-based RSI 228 | avg_gain = initial_avg['avg_gain'] 229 | avg_loss = initial_avg['avg_loss'] 230 | alpha = Decimal(1 / period) # Smoothing factor 231 | 232 | # Update moving averages using EMA formula 233 | for data in subsequent_data: 234 | avg_gain = (avg_gain * (1 - alpha) + data['gain'] * alpha) 235 | avg_loss = (avg_loss * (1 - alpha) + data['loss'] * alpha) 236 | 237 | # Prevent division by zero 238 | if avg_loss == 0: 239 | rsi = 100 240 | else: 241 | rs = avg_gain / avg_loss 242 | rsi = 100 - (100 / (1 + rs)) 243 | 244 | return { 245 | 'rsi': round(float(rsi), 4), 246 | 'avg_gain': round(float(avg_gain), 4), 247 | 'avg_loss': round(float(avg_loss), 4), 248 | 'period': period, 249 | 'days': days, 250 | } 251 | 252 | 253 | def get_stock_indicators(ticker = "AAPL", days=30): 254 | queryset = get_daily_stock_quotes_queryset(ticker, days=days) 255 | if queryset.count() == 0: 256 | raise Exception(f"Data for {ticker} not found") 257 | averages = get_daily_moving_averages(ticker, days=days, queryset=queryset) 258 | price_target = get_price_target(ticker, days=days, queryset=queryset) 259 | volume_trend = get_volume_trend(ticker, days=days, queryset=queryset) 260 | rsi_data = calculate_rsi(ticker, days=days, period=14) 261 | signals = [] 262 | if averages.get('ma_5') > averages.get('ma_20'): 263 | signals.append(1) 264 | else: 265 | signals.append(-1) 266 | if price_target.get('current_price') < price_target.get('conservative_target'): 267 | signals.append(1) 268 | else: 269 | signals.append(-1) 270 | if volume_trend.get("volume_change_percent") > 20: 271 | signals.append(1) 272 | elif volume_trend.get("volume_change_percent") < -20: 273 | signals.append(-1) 274 | else: 275 | signals.append(0) 276 | rsi = rsi_data.get('rsi') 277 | if rsi > 70: 278 | signals.append(-1) # Overbought 279 | elif rsi < 30: 280 | signals.append(1) # Oversold 281 | else: 282 | signals.append(0) 283 | return { 284 | "score": sum(signals), 285 | "ticker": ticker, 286 | "indicators": { 287 | **averages, 288 | **price_target, 289 | **volume_trend, 290 | **rsi_data, 291 | } 292 | 293 | } -------------------------------------------------------------------------------- /src/market/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from datetime import timedelta 3 | 4 | from django.apps import apps 5 | from django.utils import timezone 6 | 7 | import helpers.clients as helper_clients 8 | 9 | from .utils import batch_insert_stock_data 10 | 11 | 12 | @shared_task 13 | def sync_company_stock_quotes(company_id, days_ago = 32, date_format = "%Y-%m-%d", verbose=False): 14 | Company = apps.get_model("market", "Company") 15 | try: 16 | company_obj = Company.objects.get(id=company_id) 17 | except: 18 | company_obj = None 19 | if company_obj is None: 20 | raise Exception(f"Company Id {company_id} invalid") 21 | company_ticker = company_obj.ticker 22 | if company_ticker is None: 23 | raise Exception(f"{company_ticker} invalid") 24 | now = timezone.now() 25 | start_date = now - timedelta(days=days_ago) 26 | to_date = start_date + timedelta(days=days_ago + 1) 27 | to_date = to_date.strftime(date_format) 28 | from_date = start_date.strftime(date_format) 29 | client = helper_clients.PolygonAPIClient( 30 | ticker=company_ticker, 31 | from_date=from_date, 32 | to_date=to_date 33 | ) 34 | dataset = client.get_stock_data() 35 | if verbose: 36 | print('dataset length', len(dataset)) 37 | batch_insert_stock_data(dataset=dataset, company_obj=company_obj, verbose=verbose) 38 | 39 | 40 | 41 | @shared_task 42 | def sync_stock_data(days_ago=2): 43 | Company = apps.get_model("market", "Company") 44 | companies = Company.objects.filter(active=True).values_list('id', flat=True) 45 | for company_id in companies: 46 | sync_company_stock_quotes.delay(company_id, days_ago=days_ago) 47 | 48 | 49 | @shared_task 50 | def sync_historical_stock_data(years_ago=5, company_ids=[], use_celery=True, verbose=False): 51 | Company = apps.get_model("market", "Company") 52 | qs = Company.objects.filter(active=True) 53 | if len(company_ids) > 0: 54 | qs = qs.filter(id__in=company_ids) 55 | companies = qs.values_list('id', flat=True) 56 | for company_id in companies: 57 | days_starting_ago = 30 * 12 * years_ago 58 | batch_size = 30 59 | for i in range(30, days_starting_ago, batch_size): 60 | if verbose: 61 | print("Historical sync days ago", i) 62 | if use_celery: 63 | sync_company_stock_quotes.delay(company_id, days_ago=i, verbose=verbose) 64 | else: 65 | sync_company_stock_quotes(company_id, days_ago=i,verbose=verbose) 66 | if verbose: 67 | print(i, "done\n") -------------------------------------------------------------------------------- /src/market/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/market/utils.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | 4 | def batch_insert_stock_data( 5 | dataset, 6 | company_obj=None, 7 | batch_size=1000, 8 | verbose=False): 9 | StockQuote = apps.get_model('market', 'StockQuote') 10 | batch_size = 1000 11 | if company_obj is None: 12 | raise Exception(f"Batch failed. Company Object {company_obj} invalid") 13 | for i in range(0, len(dataset), batch_size): 14 | if verbose: 15 | print("Doing chunk", i) 16 | batch_chunk = dataset[i:i+batch_size] 17 | chunked_quotes = [] 18 | for data in batch_chunk: 19 | chunked_quotes.append( 20 | StockQuote(company=company_obj, **data) 21 | ) 22 | StockQuote.objects.bulk_create(chunked_quotes, ignore_conflicts=True) 23 | if verbose: 24 | print("finished chunk", i) 25 | return len(dataset) 26 | -------------------------------------------------------------------------------- /src/market/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | --------------------------------------------------------------------------------