├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── codecov.yml
├── docker-compose.yml
└── fastapi-prophet
├── .coveragerc
├── .dockerignore
├── Dockerfile
├── Dockerfile.prod
├── app
├── __init__.py
├── api
│ ├── __init__.py
│ ├── crud.py
│ ├── ping.py
│ ├── predictions.py
│ └── train.py
├── config.py
├── db.py
├── main.py
├── model.py
├── models
│ ├── __init__.py
│ ├── pydantic.py
│ └── sqlalchemy.py
├── plots
│ ├── GOOG_plot.png
│ └── GOOG_plot_components.png
├── prediction_engine.py
├── trained
│ └── GOOG.joblib
└── utils.py
├── coverage.xml
├── db
├── Dockerfile
└── create.sql
├── entrypoint.sh
├── pre-reqs.in
├── requirements-dev.in
├── requirements.in
├── setup.cfg
├── start.sh
└── tests
├── __init__.py
├── conftest.py
├── test_ping.py
└── test_predictions.py
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | push:
5 | branches: [master]
6 |
7 | env:
8 | IMAGE: ghcr.io/${{ github.repository }}/web-app
9 |
10 | jobs:
11 |
12 | build:
13 | name: Build Docker Image
14 | runs-on: ubuntu-18.04
15 | steps:
16 | - name: Checkout master
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Docker Buildx
20 | id: buildx
21 | uses: docker/setup-buildx-action@v1
22 |
23 | - name: Login to Registry
24 | uses: docker/login-action@v1
25 | with:
26 | registry: ghcr.io
27 | username: ${{ github.repository_owner }}
28 | password: ${{ secrets.CR_PAT }}
29 |
30 | - name: Build and Push to GhCR
31 | id: docker_build
32 | uses: docker/build-push-action@v2
33 | with:
34 | context: ./fastapi-prophet
35 | file: ./fastapi-prophet/Dockerfile.prod
36 | push: true
37 | tags: ${{ env.IMAGE }}:latest
38 | - name: Image digest
39 | run: echo ${{ steps.docker_build.outputs.digest }}
40 |
41 | test:
42 | name: Test Docker Image
43 | runs-on: ubuntu-18.04
44 | needs: build
45 | steps:
46 | - name: Checkout master
47 | uses: actions/checkout@v2
48 |
49 | - name: Login to Registry
50 | uses: docker/login-action@v1
51 | with:
52 | registry: ghcr.io
53 | username: ${{ github.repository_owner }}
54 | password: ${{ secrets.CR_PAT }}
55 |
56 | - name: Pull images
57 | run: docker pull ${{ env.IMAGE }}:latest || true
58 |
59 | - name: Run container
60 | run: |
61 | docker run \
62 | -d \
63 | --name prophet \
64 | -v "$(pwd)"/reports:/reports \
65 | -e PORT=8765 \
66 | -e ENVIRONMENT=dev \
67 | -e DATABASE_TEST_URL=sqlite:///sqlite.db \
68 | -e DATABASE_URL=sqlite:///sqlite_prod.db \
69 | -p 5003:8765 \
70 | ${{ env.IMAGE }}:latest
71 |
72 | - name: Install requirements
73 | run: docker exec prophet pip install -r ./requirements-dev.in || true
74 |
75 | - name: Print logs
76 | run: docker logs prophet
77 |
78 | - name: Run tests and generate coverage report
79 | run: docker exec prophet pytest --cov-report=xml --cov=./ -p no:warnings -vv
80 |
81 | - name: Place coverage report in shared volume
82 | run: docker exec prophet bash -c "mv coverage.xml /reports/"
83 |
84 | - name: Upload coverage report to Codecov
85 | uses: codecov/codecov-action@v1
86 | with:
87 | token: ${{ secrets.CODECOV_PAT }}
88 | files: reports/coverage.xml
89 | directory: reports
90 |
91 | - name: Flake8
92 | run: docker exec prophet python -m flake8 .
93 |
94 | - name: Black
95 | run: docker exec prophet python -m black . --check
96 |
97 | - name: isort
98 | run: docker exec prophet python -m isort . --check-only
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.png
3 | *.joblib
4 | .coverage
5 | **/*/*.db
6 | coverage.xml
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rafael Pardinas
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 Market predictions with Prophet and FastAPI
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Train prophet models and run stock market predictions for a given ticker
13 |
14 | ## Details
15 | * Dev dependencies
16 | * FastAPI: https://fastapi.tiangolo.com
17 | * Docker: https://docs.docker.com/
18 | * SQLAlchemy-Core: https://docs.sqlalchemy.org/en/14/core
19 | * Databases (Async SQLAlchemy Core queries): https://github.com/encode/databases
20 | * fbprophet (Time Series forecasting): https://facebook.github.io/prophet
21 | * yfinance (market data downloader): https://github.com/ranaroussi/yfinance
22 | * Testing:
23 | * pytest: https://docs.pytest.org/en/stable
24 | * pytest-cov: https://github.com/pytest-dev/pytest-cov
25 | * Codecov: https://docs.codecov.io
26 | * Linting and Formatting:
27 | * black: https://github.com/psf/black
28 | * flake8: https://flake8.pycqa.org/en/latest
29 | * isort: https://pycqa.github.io/isort
30 | * Continuous Integration by Github Actions
31 |
32 | ## Actions
33 | * Train the model for a ticker
34 | * Check the weekly prediction for a previously trained ticker
35 | * Check the weekly prediction for all the trained tickers
36 |
37 | #### CRUD
38 | * Create prediction for that ticker
39 | * Read results for all tickers or for individual ones
40 | * Updating the info for a ticker should be done by creating a new one due to its time-sensitiveness
41 | * Delete a specific ticker
42 |
43 | ## Future additions
44 | * Authentication
45 | * Extra information fields based on third-party content
46 | * Add plots and images if available from source
47 | * Input training timeframe
48 | * User Interface
49 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | # default
6 | target: auto
7 | threshold: 2%
8 | paths:
9 | - "fastapi-prophet/app"
10 |
11 | ignore:
12 | - "fastapi-prophet/tests/conftest.py"
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 |
5 | web:
6 | build: ./fastapi-prophet
7 | command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
8 | volumes:
9 | - ./fastapi-prophet/:/usr/src/app/
10 | ports:
11 | - 8006:8000
12 | environment:
13 | - ENVIRONMENT:dev
14 | - TESTING:0
15 | - DATABASE_URL=postgresql://prophet_fastapi:hello_fastapi@prophet-db:5432/prophet_dev
16 | - DATABASE_TEST_URL=postgresql://prophet_fastapi:hello_fastapi@prophet-db:5432/prophet_test
17 | depends_on:
18 | - prophet-db
19 |
20 | prophet-db:
21 | build:
22 | context: ./fastapi-prophet/db
23 | dockerfile: Dockerfile
24 | expose:
25 | - 5432
26 | environment:
27 | - POSTGRES_USER=prophet_fastapi
28 | - POSTGRES_PASSWORD=hello_fastapi
29 |
30 | test:
31 | build:
32 | context: ./fastapi-prophet
33 | dockerfile: ./Dockerfile.prod
34 | command: gunicorn --bind 0.0.0.0:8000 app.main:app -k uvicorn.workers.UvicornWorker
35 | volumes:
36 | - ./fastapi-prophet/:/usr/src/app/
37 | ports:
38 | - 8004:8000
39 | environment:
40 | - PORT:8765
41 | - ENVIRONMENT:dev
42 | - TESTING:1
43 | - DATABASE_URL=sqlite:///sqlite_prod.db
44 | - DATABASE_TEST_URL=sqlite:///sqlite_test.db
45 | - CODECOV_TOKEN=ffff585d-c5e3-4327-8d28-295b2f5d7f17
46 |
--------------------------------------------------------------------------------
/fastapi-prophet/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit = tests/*
3 | branch = True
4 |
--------------------------------------------------------------------------------
/fastapi-prophet/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | Dockerfile
3 | Dockerfile.prod
4 | .coverage
5 |
--------------------------------------------------------------------------------
/fastapi-prophet/Dockerfile:
--------------------------------------------------------------------------------
1 | ###########
2 | # BUILDER #
3 | ###########
4 |
5 | FROM python:3.8.7-slim-buster as builder
6 |
7 | RUN apt-get update \
8 | && apt-get install -y --no-install-recommends build-essential \
9 | && apt-get clean
10 |
11 | WORKDIR /usr/src/app
12 |
13 | RUN python -m venv /opt/venv
14 | # Make sure we use the virtualenv:
15 | ENV PATH="/opt/venv/bin:$PATH"
16 |
17 | RUN pip install --upgrade pip && pip install pip-tools
18 | COPY ./pre-reqs.in .
19 | COPY ./requirements.in .
20 | COPY ./requirements-dev.in .
21 | RUN pip-compile pre-reqs.in > pre-reqs.txt
22 | RUN pip-compile requirements.in > requirements.txt
23 | RUN pip-compile requirements-dev.in > requirements-dev.txt
24 | RUN pip-sync pre-reqs.txt requirements.txt requirements-dev.txt
25 | RUN pip install -r pre-reqs.txt
26 | RUN pip install -r requirements.txt
27 | RUN pip install -r requirements-dev.txt
28 |
29 | #########
30 | # FINAL #
31 | #########
32 |
33 | # pull official base image
34 | FROM python:3.8.7-slim-buster
35 |
36 | RUN apt-get update \
37 | && apt-get install -y --no-install-recommends postgresql netcat \
38 | && apt-get clean
39 |
40 | ## set environment variables
41 | ENV PYTHONDONTWRITEBYTECODE 1
42 | ENV PYTHONUNBUFFERED 1
43 |
44 | COPY --from=builder /opt/venv /opt/venv
45 |
46 | # Make sure we use the virtualenv:
47 | ENV PATH="/opt/venv/bin:$PATH"
48 |
49 | WORKDIR /usr/src/app
50 |
51 | COPY . .
52 |
53 | COPY ./entrypoint.sh .
54 | RUN chmod +x /usr/src/app/entrypoint.sh
55 |
56 | ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
57 |
--------------------------------------------------------------------------------
/fastapi-prophet/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | ###########
2 | # BUILDER #
3 | ###########
4 |
5 | FROM python:3.8.7-slim-buster as builder
6 |
7 | RUN apt-get update \
8 | && apt-get install -y --no-install-recommends build-essential \
9 | && apt-get clean
10 |
11 | WORKDIR /usr/src/app
12 |
13 | # create venv
14 | RUN python -m venv /opt/venv
15 | # activate it
16 | ENV PATH="/opt/venv/bin:$PATH"
17 |
18 | RUN pip install --upgrade pip && pip install pip-tools
19 | COPY ./pre-reqs.in .
20 | COPY ./requirements.in .
21 | RUN pip-compile pre-reqs.in > pre-reqs.txt
22 | RUN pip-compile requirements.in > requirements.txt
23 | RUN pip-sync pre-reqs.txt requirements.txt
24 | RUN pip install -r pre-reqs.txt
25 | RUN pip install -r requirements.txt
26 |
27 | #########
28 | # FINAL #
29 | #########
30 |
31 | # pull official base image
32 | FROM python:3.8.7-slim-buster
33 |
34 | RUN apt-get update \
35 | && apt-get install -y --no-install-recommends postgresql netcat curl \
36 | && apt-get clean
37 |
38 | ## set environment variables
39 | ENV PYTHONDONTWRITEBYTECODE 1
40 | ENV PYTHONUNBUFFERED 1
41 | ENV ENVIRONMENT prod
42 | ENV TESTING 0
43 |
44 | COPY --from=builder /opt/venv /opt/venv
45 |
46 | # activate the venv
47 | ENV PATH="/opt/venv/bin:$PATH"
48 |
49 | RUN pip install --upgrade pip
50 | RUN pip install "uvicorn[standard]==0.13.1" "gunicorn==20.0.4"
51 | RUN pip install "aiosqlite==0.17.0"
52 |
53 | WORKDIR /usr/src/app
54 |
55 | COPY . .
56 |
57 | COPY ./start.sh .
58 | RUN chmod +x /usr/src/app/start.sh
59 |
60 | CMD ["/usr/src/app/start.sh"]
61 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/__init__.py
--------------------------------------------------------------------------------
/fastapi-prophet/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/api/__init__.py
--------------------------------------------------------------------------------
/fastapi-prophet/app/api/crud.py:
--------------------------------------------------------------------------------
1 | from app.models.pydantic import StockIn
2 | from app.models.sqlalchemy import predictions
3 |
4 |
5 | async def post(payload: StockIn, database):
6 | query = predictions.insert().values(ticker=payload.ticker)
7 | return await database.execute(query=query)
8 |
9 |
10 | async def get(id: int, database):
11 | query = predictions.select().where(id == predictions.c.id)
12 | return await database.fetch_one(query=query)
13 |
14 |
15 | async def get_all(database):
16 | query = predictions.select()
17 | return await database.fetch_all(query=query)
18 |
19 |
20 | async def delete(id: int, database):
21 | query = predictions.delete().where(id == predictions.c.id)
22 | return await database.execute(query=query)
23 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/api/ping.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 |
3 | from app.config import Settings, get_settings
4 |
5 | router = APIRouter()
6 |
7 |
8 | # health-check
9 | @router.get("/ping")
10 | async def pong(settings: Settings = Depends(get_settings)):
11 | return {
12 | "ping": "pong!",
13 | "environment": settings.environment,
14 | "testing": settings.testing,
15 | }
16 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/api/predictions.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 | from typing import List
4 |
5 | from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Path
6 |
7 | from app.api import crud
8 | from app.db import get_db
9 | from app.models.pydantic import PredictionSchema, StockIn, StockOut
10 | from app.prediction_engine import generate_prediction
11 | from app.utils import pred_to_dict
12 |
13 | router = APIRouter()
14 |
15 |
16 | BASE_DIR = pathlib.Path(__file__).resolve(strict=True).parent.parent
17 | TRAINED_DIR = pathlib.Path(BASE_DIR) / "trained"
18 |
19 |
20 | @router.post("/", response_model=StockOut, status_code=201)
21 | async def create_prediction(
22 | payload: StockIn,
23 | background_tasks: BackgroundTasks,
24 | database=Depends(get_db),
25 | ):
26 | model_file = TRAINED_DIR / f"{payload.ticker}.joblib"
27 | for entry in os.listdir(TRAINED_DIR):
28 | if os.path.isfile(os.path.join(TRAINED_DIR, entry)):
29 | print(entry)
30 | if not model_file.exists():
31 | raise HTTPException(
32 | status_code=405,
33 | detail="Not Allowed. A model must be trained to process this request.",
34 | )
35 | prediction_id = await crud.post(payload, database)
36 |
37 | background_tasks.add_task(
38 | generate_prediction, prediction_id, payload.ticker, database
39 | )
40 |
41 | response_object = {"id": prediction_id, "ticker": payload.ticker}
42 |
43 | return response_object
44 |
45 |
46 | @router.get("/{id}/", response_model=PredictionSchema)
47 | async def get_prediction(
48 | id: int = Path(..., gt=0), database=Depends(get_db)
49 | ) -> PredictionSchema:
50 | prediction_items = await crud.get(id, database)
51 | if not prediction_items:
52 | raise HTTPException(status_code=404, detail="Prediction not found")
53 |
54 | return pred_to_dict(prediction_items)
55 |
56 |
57 | @router.get("/", response_model=List[PredictionSchema])
58 | async def get_all_predictions(
59 | database=Depends(get_db),
60 | ) -> List[PredictionSchema]:
61 | prediction_items = await crud.get_all(database)
62 | if not prediction_items:
63 | raise HTTPException(
64 | status_code=404, detail="No predictions have been created at this time"
65 | )
66 | return [pred_to_dict(pred) for pred in prediction_items]
67 |
68 |
69 | @router.delete("/{id}/", response_model=PredictionSchema)
70 | async def delete_prediction(
71 | id: int = Path(..., gt=0), database=Depends(get_db)
72 | ) -> PredictionSchema:
73 | prediction = await crud.get(id, database)
74 | if not prediction:
75 | raise HTTPException(status_code=404, detail="Prediction not found")
76 |
77 | await crud.delete(id, database)
78 |
79 | return pred_to_dict(prediction)
80 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/api/train.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, BackgroundTasks
2 |
3 | from app.model import train
4 | from app.models.pydantic import StockIn
5 |
6 | router = APIRouter()
7 |
8 |
9 | @router.post("/", status_code=201)
10 | async def train_prediction(
11 | payload: StockIn,
12 | background_tasks: BackgroundTasks,
13 | ):
14 |
15 | background_tasks.add_task(train, payload.ticker)
16 |
17 | return payload
18 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/config.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from functools import lru_cache
4 |
5 | from pydantic import BaseSettings
6 |
7 | log = logging.getLogger(__name__)
8 |
9 |
10 | class Settings(BaseSettings):
11 | environment: str = os.getenv("ENVIRONMENT", "dev")
12 | testing: bool = os.getenv("TESTING", 0)
13 | database_url: str = os.getenv("DATABASE_URL")
14 |
15 |
16 | @lru_cache()
17 | def get_settings() -> BaseSettings:
18 | log.info("Loading config settings from the environment...")
19 | return Settings()
20 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/db.py:
--------------------------------------------------------------------------------
1 | # import os
2 | from typing import Optional
3 |
4 | from databases import Database
5 | from sqlalchemy import create_engine
6 |
7 | from app.config import get_settings
8 |
9 | database = Optional[Database]
10 |
11 |
12 | settings = get_settings()
13 |
14 |
15 | def get_eng():
16 | db_url = settings.database_url
17 | if settings.testing:
18 | engine = create_engine(db_url, connect_args={"check_same_thread": False})
19 | else:
20 | engine = create_engine(db_url)
21 | return engine
22 |
23 |
24 | def get_db() -> Database:
25 | global database
26 | db_url = settings.database_url
27 | if settings.testing:
28 | database = Database(db_url, force_rollback=True)
29 | else:
30 | database = Database(db_url)
31 |
32 | return database
33 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from fastapi import FastAPI
4 |
5 | from app.api import ping, predictions, train
6 | from app.db import get_db, get_eng
7 | from app.models.sqlalchemy import metadata
8 |
9 | log = logging.getLogger(__name__)
10 |
11 |
12 | def create_application() -> FastAPI:
13 | application = FastAPI()
14 | application.include_router(ping.router)
15 | application.include_router(predictions.router, prefix="/predict", tags=["predict"])
16 | application.include_router(train.router, prefix="/train", tags=["train"])
17 | return application
18 |
19 |
20 | metadata.create_all(get_eng())
21 | database = get_db()
22 | app = create_application()
23 |
24 |
25 | @app.on_event("startup")
26 | async def startup():
27 | log.info("Starting up...")
28 | await database.connect()
29 |
30 |
31 | @app.on_event("shutdown")
32 | async def shutdown():
33 | log.info("Shutting down...")
34 | await database.disconnect()
35 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/model.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import pathlib
3 |
4 | import joblib
5 | import pandas as pd
6 | import yfinance as yf
7 | from prophet import Prophet
8 |
9 | BASE_DIR = pathlib.Path(__file__).resolve(strict=True).parent
10 | TRAINED_DIR = pathlib.Path(BASE_DIR) / "trained"
11 | PLOTS_DIR = pathlib.Path(BASE_DIR) / "plots"
12 | TODAY = datetime.date.today()
13 |
14 |
15 | def train(ticker="MSFT"):
16 | data = yf.download(ticker, "2021-02-01", TODAY.strftime("%Y-%m-%d"))
17 | data.head()
18 | data["Adj Close"].plot(title=f"{ticker} Stock Adjust Closing Price")
19 |
20 | df_forecast = data.copy()
21 | df_forecast.reset_index(inplace=True)
22 | df_forecast["ds"] = df_forecast["Date"]
23 | df_forecast["y"] = df_forecast["Adj Close"]
24 | df_forecast = df_forecast[["ds", "y"]]
25 | df_forecast
26 |
27 | model = Prophet()
28 | model.fit(df_forecast)
29 |
30 | joblib.dump(model, TRAINED_DIR / f"{ticker}.joblib")
31 |
32 |
33 | async def predict(ticker="MSFT", days=7):
34 | model_file = TRAINED_DIR / f"{ticker}.joblib"
35 | if not model_file.exists():
36 | return False
37 |
38 | model = joblib.load(model_file)
39 |
40 | future = TODAY + datetime.timedelta(days=days)
41 |
42 | dates = pd.date_range(
43 | start="2020-01-01",
44 | end=future.strftime("%m/%d/%Y"),
45 | )
46 | df = pd.DataFrame({"ds": dates})
47 |
48 | forecast = model.predict(df)
49 |
50 | model.plot(forecast).savefig(PLOTS_DIR / f"{ticker}_plot.png")
51 | model.plot_components(forecast).savefig(PLOTS_DIR / f"{ticker}_plot_components.png")
52 |
53 | return forecast.tail(days).to_dict("records")
54 |
55 |
56 | def convert(prediction_list):
57 | output = {}
58 | for data in prediction_list:
59 | date = data["ds"].strftime("%m/%d/%Y")
60 | output[date] = data["trend"]
61 | return output
62 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/models/__init__.py
--------------------------------------------------------------------------------
/fastapi-prophet/app/models/pydantic.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class StockIn(BaseModel):
7 | ticker: str
8 |
9 |
10 | class StockOut(StockIn):
11 | id: int
12 |
13 |
14 | class PredictionSchema(StockOut):
15 | prediction: dict
16 | created_date: datetime
17 |
18 | class Config:
19 | orm_mode = True
20 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/models/sqlalchemy.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table
2 | from sqlalchemy.sql import func
3 |
4 | metadata = MetaData()
5 |
6 | predictions = Table(
7 | "predictions",
8 | metadata,
9 | Column("id", Integer, primary_key=True),
10 | Column("ticker", String(6)),
11 | Column("prediction", String),
12 | Column("created_date", DateTime, default=func.now(), nullable=False),
13 | )
14 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/plots/GOOG_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/plots/GOOG_plot.png
--------------------------------------------------------------------------------
/fastapi-prophet/app/plots/GOOG_plot_components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/plots/GOOG_plot_components.png
--------------------------------------------------------------------------------
/fastapi-prophet/app/prediction_engine.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from fastapi import HTTPException
4 |
5 | from app.model import convert, predict
6 | from app.models.pydantic import StockIn
7 | from app.models.sqlalchemy import predictions
8 |
9 |
10 | async def generate_prediction(id: int, ticker: StockIn, database):
11 | prediction_list = await predict(ticker)
12 | if not prediction_list:
13 | raise HTTPException(status_code=404, detail="No train model found")
14 | prediction_data = json.dumps(convert(prediction_list))
15 |
16 | query = (
17 | predictions.update()
18 | .where(id == predictions.c.id)
19 | .values(ticker=ticker, prediction=prediction_data)
20 | )
21 | return await database.execute(query=query)
22 |
--------------------------------------------------------------------------------
/fastapi-prophet/app/trained/GOOG.joblib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/app/trained/GOOG.joblib
--------------------------------------------------------------------------------
/fastapi-prophet/app/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | def pred_to_dict(prediction_items):
5 | pred_dict = {}
6 | for k, v in prediction_items.items():
7 | pred_dict[k] = json.loads(v) if k == "prediction" else v
8 | return pred_dict
9 |
--------------------------------------------------------------------------------
/fastapi-prophet/coverage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | /usr/src/app
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
--------------------------------------------------------------------------------
/fastapi-prophet/db/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:13-alpine
2 |
3 | ADD create.sql /docker-entrypoint-initdb.d
4 |
--------------------------------------------------------------------------------
/fastapi-prophet/db/create.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE prophet_dev;
2 | CREATE DATABASE prophet_test;
3 |
--------------------------------------------------------------------------------
/fastapi-prophet/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | echo "Waiting for PostgreSQL..."
4 |
5 | while ! nc -z prophet-db 5432; do
6 | sleep 0.1
7 | done
8 |
9 | echo "PostgreSQL started"
10 |
11 | exec "$@"
12 |
--------------------------------------------------------------------------------
/fastapi-prophet/pre-reqs.in:
--------------------------------------------------------------------------------
1 | wheel==0.37.0
2 | Cython>=0.22
3 | # cmdstanpy>=0.9.5
4 | psycopg2-binary==2.8.6
5 | numpy>=1.20.1
6 | # pandas==1.3.3
7 | matplotlib>=2.0.0
8 | LunarCalendar>=0.0.9
9 | convertdate>=2.1.2
10 | holidays>=0.10.2
11 | pystan==2.19.1.1
12 | tqdm>=4.36.1
13 |
--------------------------------------------------------------------------------
/fastapi-prophet/requirements-dev.in:
--------------------------------------------------------------------------------
1 | aiosqlite==0.17.0
2 | databases[sqlite]==0.5.2
3 | pytest==6.2.0
4 | pytest-cov>=2.10.0
5 | flake8===3.8.4
6 | black==20.8b1
7 | isort==5.6.4
8 | pytest-xdist==2.2.0
9 | pytest-asyncio>=0.14.0
10 |
--------------------------------------------------------------------------------
/fastapi-prophet/requirements.in:
--------------------------------------------------------------------------------
1 | databases[postgresql]==0.5.2
2 | fastapi>=0.65.2
3 | prophet==1.0.1
4 | joblib==1.0.0
5 | plotly==4.14.3
6 | yfinance==0.1.63
7 | uvicorn[standard]==0.15.0
8 | gunicorn==20.1.0
9 |
--------------------------------------------------------------------------------
/fastapi-prophet/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 119
3 |
--------------------------------------------------------------------------------
/fastapi-prophet/start.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # set -e
4 |
5 | # if [ -f /app/app/main.py ]; then
6 | # DEFAULT_MODULE_NAME=app.main
7 | # elif [ -f /app/main.py ]; then
8 | # DEFAULT_MODULE_NAME=main
9 | # fi
10 | # MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
11 | # VARIABLE_NAME=${VARIABLE_NAME:-app}
12 | # export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}
13 |
14 | # if [ -f /app/gunicorn_conf.py ]; then
15 | # DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py
16 | # elif [ -f /app/app/gunicorn_conf.py ]; then
17 | # DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py
18 | # else
19 | # DEFAULT_GUNICORN_CONF=/gunicorn_conf.py
20 | # fi
21 | # export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF}
22 | # export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}
23 |
24 | # # Start Gunicorn
25 | # exec gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE"
26 |
27 | exec gunicorn --bind 0.0.0.0:$PORT app.main:app -k uvicorn.workers.UvicornWorker
28 |
--------------------------------------------------------------------------------
/fastapi-prophet/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rafapi/fastapi-prophet/66814e7f3456f10515d627b1ce80accb1c4f095d/fastapi-prophet/tests/__init__.py
--------------------------------------------------------------------------------
/fastapi-prophet/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # import asyncio
4 | import pytest
5 | from fastapi.testclient import TestClient
6 |
7 | from app.config import get_settings
8 | from app.db import get_db, get_eng
9 | from app.main import create_application
10 | from app.models.sqlalchemy import metadata
11 |
12 | settings = get_settings()
13 |
14 |
15 | @pytest.fixture(scope="module")
16 | def test_app():
17 | settings.testing = True
18 | settings.environment = "test"
19 | settings.database_url = os.getenv("DATABASE_TEST_URL")
20 | app = create_application()
21 | with TestClient(app) as test_client:
22 | yield test_client
23 |
24 |
25 | @pytest.fixture(scope="module")
26 | def db():
27 | metadata.create_all(get_eng())
28 | database = get_db()
29 | yield database
30 |
--------------------------------------------------------------------------------
/fastapi-prophet/tests/test_ping.py:
--------------------------------------------------------------------------------
1 | def test_ping(test_app):
2 | response = test_app.get("/ping")
3 | assert response.status_code == 200
4 | assert response.json() == {
5 | "ping": "pong!",
6 | "environment": "test",
7 | "testing": True,
8 | }
9 |
--------------------------------------------------------------------------------
/fastapi-prophet/tests/test_predictions.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from datetime import datetime
4 |
5 | from app.api import crud
6 | from app.config import get_settings
7 | from app.utils import pred_to_dict
8 |
9 |
10 | def test_db_test_url(test_app):
11 | settings = get_settings()
12 | assert settings.database_url == os.environ.get("DATABASE_TEST_URL")
13 |
14 |
15 | def test_create_prediction(test_app, db):
16 | test_request_payload = {"ticker": "GOOG"}
17 |
18 | response = test_app.post("/predict/", json.dumps(test_request_payload), db)
19 |
20 | prediction_id = response.json()["id"]
21 |
22 | assert response.status_code == 201
23 | assert response.json() == {"id": prediction_id, "ticker": "GOOG"}
24 |
25 |
26 | def test_create_prediction_invalid_json(test_app):
27 | response = test_app.post("/predict/", data=json.dumps({2: "GOOG"}))
28 | assert response.status_code == 422
29 |
30 |
31 | def test_read_prediction(test_app, db, monkeypatch):
32 | test_data = {
33 | "id": 1,
34 | "ticker": "MSFT",
35 | "prediction": json.dumps(
36 | {
37 | "07/22/2020": 212.29389088938012,
38 | "07/23/2020": 212.75441373941516,
39 | "07/24/2020": 213.21493658945016,
40 | "07/25/2020": 213.6754594394852,
41 | "07/26/2020": 214.1359822895202,
42 | "07/27/2020": 214.59650513955518,
43 | "07/28/2020": 215.05702798959027,
44 | }
45 | ),
46 | "created_date": datetime.utcnow().isoformat(),
47 | }
48 |
49 | async def mock_get(id, db):
50 | return test_data
51 |
52 | monkeypatch.setattr(crud, "get", mock_get)
53 |
54 | response = test_app.get("/predict/1/")
55 | assert response.status_code == 200
56 | assert response.json() == pred_to_dict(test_data)
57 |
58 |
59 | # def test_read_all_predictions(test_app, db):
60 | # test_request_payload = {"ticker": "GOOG"}
61 |
62 | # response_post = test_app.post("/predict/", json.dumps(test_request_payload), db)
63 |
64 | # response_get = test_app.get("/predict/")
65 | # assert response_get.status_code == 200
66 | # response_list = response_get.json()
67 |
68 | # prediction_id = response_post.json()["id"]
69 | # assert len(list(filter(lambda d: d["id"] == prediction_id, response_list))) == 1
70 |
71 |
72 | def test_read_prediction_incorrect_id(test_app, db):
73 | response = test_app.get("/predict/999/")
74 | assert response.status_code == 404
75 | assert response.json()["detail"] == "Prediction not found"
76 |
77 |
78 | # def test_delete_prediction(test_app, db):
79 | # test_request_payload = {"ticker": "GOOG"}
80 |
81 | # post_response = test_app.post("/predict/", json.dumps(test_request_payload), db)
82 | # prediction_id = post_response.json()["id"]
83 |
84 | # del_response = test_app.delete(f"/predict/{prediction_id}/")
85 |
86 | # assert del_response.json()["id"] == prediction_id
87 |
88 |
89 | def test_delete_prediction_incorrect_id(test_app, db):
90 | response = test_app.delete("/predict/999/")
91 | assert response.status_code == 404
92 | assert response.json()["detail"] == "Prediction not found"
93 |
94 | response = test_app.delete("/predict/0/")
95 | assert response.status_code == 422
96 | assert response.json() == {
97 | "detail": [
98 | {
99 | "loc": ["path", "id"],
100 | "msg": "ensure this value is greater than 0",
101 | "type": "value_error.number.not_gt",
102 | "ctx": {"limit_value": 0},
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------