Skip to content

Commit 8af16de

Browse files
committed
Modified python code files
1 parent e84371a commit 8af16de

11 files changed

Lines changed: 222 additions & 21 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"polars>=1.12.0",
1616
"pyarrow>=18.0.0",
1717
"pydantic>=2.9.2",
18+
"python-dotenv>=1.0.1",
1819
"pyyaml>=6.0.2",
1920
]
2021

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
def main() -> None:
2-
print("Hello from stock-valuation-app!")
1+
from fastapi import FastAPI
2+
from .api.routes import router
3+
from .ui.dashboard import app as dash_app
4+
5+
app = FastAPI()
6+
app.include_router(router)
7+
8+
# Mount Dash app
9+
app.mount("/dashboard", dash_app.server)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from fastapi import APIRouter, Depends
2+
from ..data.fmp_client import FMPClient
3+
from ..services.valuation import (
4+
calculate_growth_rates,
5+
is_quality_dividend_growth_stock,
6+
is_undervalued,
7+
)
8+
9+
router = APIRouter()
10+
11+
12+
async def get_fmp_client():
13+
return FMPClient("YOUR_API_KEY")
14+
15+
16+
@router.get("/stock/{symbol}")
17+
async def get_stock_valuation(
18+
symbol: str, fmp_client: FMPClient = Depends(get_fmp_client)
19+
):
20+
financial_data = await fmp_client.get_financial_data(symbol)
21+
growth_rates = calculate_growth_rates(financial_data)
22+
23+
# Assuming we have a way to get the current P/E ratio
24+
current_pe = 15 # This should be fetched from the API
25+
26+
return {
27+
"symbol": symbol,
28+
"growth_rates": growth_rates,
29+
"is_quality_dividend_growth_stock": is_quality_dividend_growth_stock(
30+
growth_rates
31+
),
32+
"is_undervalued": is_undervalued(growth_rates, current_pe),
33+
}

src/stock_valuation_app/config.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
api:
22
base_url: "https://financialmodelingprep.com/api/v3"
3+
annual_ratios: "ratios/"
4+
annual_financial_growth: "financial-growth/"
5+
rating: "rating/"
36

47
valuation:
58
default_pe_ratio: 15
@@ -15,5 +18,8 @@ database:
1518
file: "stock_data.duckdb"
1619

1720
logging:
18-
level: "INFO"
19-
file: "app.log"
21+
level: INFO
22+
file: app.log
23+
filemode: "w"
24+
format: "%(asctime)s - %(levelname)s - %(message)s"
25+
datefmt: "%Y-%m-%d %H:%M:%S"
Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
1+
from dataclasses import dataclass, field
2+
from urllib.parse import urljoin
13
import httpx
4+
from typing import Any, Optional
25
from pydantic import BaseModel
6+
from stock_valuation_app.utils import utils
37

48

5-
class FinancialData(BaseModel):
6-
revenue: float
7-
netIncome: float
8-
freeCashFlow: float
9-
dividendsPaid: float
9+
@dataclass
10+
class FMPClient:
11+
"""A client for interacting with the Financial Modeling Prep API."""
12+
base_url: str = field(default_factory=utils.get_base_url)
13+
api_key: str = field(default_factory=utils.get_api_key)
1014

1115

12-
class FMPClient:
13-
def __init__(self, api_key: str):
14-
self.base_url = "https://financialmodelingprep.com/api/v3"
15-
self.api_key = api_key
16+
async def fetch_data(self, endpoint: str, symbol: str, period: Optional[str] = "annual"): # params: dict[str, Any]
17+
"""Fetch data from the Financial Modeling Prep API."""
18+
base_endpoint = urljoin(self.base_url, endpoint)
19+
if period is None:
20+
url = f"{base_endpoint}/{symbol}?apikey={self.api_key}"
21+
else:
22+
url = f"{base_endpoint}/{symbol}?period={period}&apikey={self.api_key}"
1623

17-
async def get_financial_data(
18-
self, symbol: str, limit: int = 10
19-
) -> list[FinancialData]:
20-
url = f"{self.base_url}/income-statement/{symbol}?limit={limit}&apikey={self.api_key}"
2124
async with httpx.AsyncClient() as client:
2225
response = await client.get(url)
2326
response.raise_for_status()
2427
data = response.json()
25-
return [FinancialData(**item) for item in data]
28+
return data
29+
30+
api_client = FMPClient()
31+
rating_endpoint = utils.get_endpoint("rating")
32+
data = api_client.fetch_data(rating_endpoint, "AAPL")
33+
print(data)

src/stock_valuation_app/main.py

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import polars as pl
2+
from ..data.fmp_client import FinancialData
3+
4+
5+
def calculate_growth_rates(data: list[FinancialData]) -> dict[str, dict[str, float]]:
6+
df = pl.DataFrame([d.dict() for d in data])
7+
8+
metrics = ["revenue", "netIncome", "freeCashFlow", "dividendsPaid"]
9+
periods = [3, 5, 10]
10+
11+
results = {}
12+
for metric in metrics:
13+
metric_results = {}
14+
for period in periods:
15+
if len(df) >= period:
16+
growth_rate = (df[metric][0] / df[metric][period - 1]) ** (
17+
1 / period
18+
) - 1
19+
metric_results[f"{period}y"] = growth_rate
20+
results[metric] = metric_results
21+
22+
return results
23+
24+
25+
def is_quality_dividend_growth_stock(growth_rates: dict[str, dict[str, float]]) -> bool:
26+
revenue_growth = growth_rates["revenue"]["5y"]
27+
dividend_growth = growth_rates["dividendsPaid"]["5y"]
28+
return revenue_growth > 0.05 and dividend_growth > 0.05
29+
30+
31+
def is_undervalued(
32+
growth_rates: dict[str, dict[str, float]], current_pe: float
33+
) -> bool:
34+
earnings_growth = growth_rates["netIncome"]["5y"]
35+
fair_pe = earnings_growth * 100 # PEG ratio of 1
36+
return current_pe < fair_pe
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import dash
2+
from dash import dcc, html
3+
from dash.dependencies import Input, Output
4+
import plotly.graph_objs as go
5+
import httpx
6+
7+
app = dash.Dash(__name__)
8+
9+
app.layout = html.Div(
10+
[
11+
html.H1("Stock Valuation Dashboard"),
12+
dcc.Input(id="stock-input", type="text", placeholder="Enter stock symbol"),
13+
html.Button("Analyze", id="analyze-button"),
14+
html.Div(id="valuation-output"),
15+
dcc.Graph(id="growth-chart"),
16+
]
17+
)
18+
19+
20+
@app.callback(
21+
[Output("valuation-output", "children"), Output("growth-chart", "figure")],
22+
[Input("analyze-button", "n_clicks")],
23+
[dash.dependencies.State("stock-input", "value")],
24+
)
25+
def update_valuation(n_clicks, symbol):
26+
if n_clicks is None or not symbol:
27+
return dash.no_update, dash.no_update
28+
29+
# Fetch data from our FastAPI endpoint
30+
response = httpx.get(f"http://localhost:8000/stock/{symbol}")
31+
data = response.json()
32+
33+
# Create valuation output
34+
valuation_output = [
35+
html.P(
36+
f"Is quality dividend growth stock: {data['is_quality_dividend_growth_stock']}"
37+
),
38+
html.P(f"Is undervalued: {data['is_undervalued']}"),
39+
]
40+
41+
# Create growth chart
42+
traces = []
43+
for metric, rates in data["growth_rates"].items():
44+
trace = go.Bar(x=list(rates.keys()), y=list(rates.values()), name=metric)
45+
traces.append(trace)
46+
47+
layout = go.Layout(title="Growth Rates", barmode="group")
48+
figure = go.Figure(data=traces, layout=layout)
49+
50+
return valuation_output, figure
51+
52+
53+
if __name__ == "__main__":
54+
app.run_server(debug=True)

src/stock_valuation_app/utils/__init__.py

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
from pathlib import Path
3+
from dotenv import load_dotenv
4+
import yaml
5+
6+
# Load environment variables from .env file
7+
load_dotenv()
8+
# Define the path to the config.yml file
9+
config_file = Path("../config.yml")
10+
11+
def load_config():
12+
"""Load configuration from a YAML file."""
13+
with open(f"{config_file}", "r", encoding="utf-8") as file:
14+
config = yaml.safe_load(file)
15+
return config
16+
17+
18+
def get_section_config(section: str):
19+
"""Get configuration for a specific section."""
20+
match section:
21+
case "api":
22+
return load_config().get("api")
23+
case "valuation":
24+
return load_config().get("valuation")
25+
case "dashboard":
26+
return load_config().get("dashboard")
27+
case "database":
28+
return load_config().get("database")
29+
case "logging":
30+
return load_config().get("logging")
31+
case _:
32+
raise ValueError(f"Invalid section: {section}")
33+
34+
def get_endpoint(url: str) -> str:
35+
"""Get endpoints from the configuration."""
36+
api_config = get_section_config("api")
37+
return api_config.get(f"{url}")
38+
39+
def get_base_url() -> str:
40+
"""Get the base URL from the configuration."""
41+
return get_endpoint("base_url")
42+
43+
def get_api_key() -> str:
44+
"""Get the API key from the .env file."""
45+
return os.getenv("FMP_API_KEY", "")

0 commit comments

Comments
 (0)