Skip to content

Commit 88bba53

Browse files
authored
Start Fama French integration
1 parent 5076fc8 commit 88bba53

File tree

6 files changed

+2331
-0
lines changed

6 files changed

+2331
-0
lines changed

demo-risk/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Fama French Factors & Research Portfolios Data
2+
3+
## Installation
4+
5+
Install this in an environment with a version of Python between 3.9-3.12, inclusively.
6+
7+
- Run, `pip install -e .`
8+
9+
- Run, `demo-risk`
10+
11+
- Open the OpenBB Workspace.
12+
13+
- Add as a new custom backend.
14+
15+
- Go to the templates page, click Refresh button.
16+
17+
- Click on the "Factors" template.
18+
19+
- Enjoy your burrito.

demo-risk/demo_risk/__init__.py

Whitespace-only changes.

demo-risk/demo_risk/app.py

+319
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
"""Main application."""
2+
3+
import asyncio
4+
5+
from typing import Annotated, Literal, Optional
6+
7+
from fastapi import FastAPI, Query
8+
9+
from demo_risk.constants import DATASET_CHOICES
10+
from demo_risk.utils import (
11+
FactorsDoc,
12+
get_portfolio_data,
13+
)
14+
15+
DATASETS = {}
16+
LOADED_PORTFOLIO = None
17+
LOADED_FACTORS = None
18+
19+
app = FastAPI()
20+
21+
22+
@app.get("/templates.json")
23+
async def get_templates():
24+
"""Get templates."""
25+
return [
26+
{
27+
"name": "Factors",
28+
"img": "",
29+
"img_dark": "",
30+
"img_light": "",
31+
"description": "Fama-French Research Portfolio and Factor Data",
32+
"allowCustomization": True,
33+
"tabs": {
34+
"": {
35+
"id": "",
36+
"name": "",
37+
"layout": [
38+
{
39+
"i": "fama_french_info_custom_obb",
40+
"x": 0,
41+
"y": 0,
42+
"w": 12,
43+
"h": 31,
44+
},
45+
{
46+
"i": "load_portfolios_custom_obb",
47+
"x": 12,
48+
"y": 0,
49+
"w": 28,
50+
"h": 16,
51+
"state": {
52+
"params": {
53+
"start_date": "1995-01-01",
54+
"end_date": "2025-12-31",
55+
},
56+
"chartView": {"enabled": False, "chartType": "line"},
57+
"columnState": {
58+
"default_": {
59+
"columnPinning": {
60+
"leftColIds": ["Date"],
61+
"rightColIds": [],
62+
}
63+
}
64+
},
65+
},
66+
},
67+
{
68+
"i": "load_factors_custom_obb",
69+
"x": 12,
70+
"y": 16,
71+
"w": 25,
72+
"h": 15,
73+
"state": {
74+
"params": {
75+
"start_date": "1995-01-01",
76+
"end_date": "2025-12-31",
77+
},
78+
"chartView": {"enabled": False, "chartType": "line"},
79+
"columnState": {
80+
"default_": {
81+
"columnPinning": {
82+
"leftColIds": ["Date"],
83+
"rightColIds": [],
84+
}
85+
}
86+
},
87+
},
88+
},
89+
],
90+
}
91+
},
92+
"groups": [
93+
{
94+
"name": "Group 3",
95+
"type": "param",
96+
"paramName": "frequency",
97+
"defaultValue": "Monthly",
98+
"widgetIds": [
99+
"load_portfolios_custom_obb",
100+
"load_factors_custom_obb",
101+
],
102+
},
103+
{
104+
"name": "Group 2",
105+
"type": "param",
106+
"paramName": "start_date",
107+
"defaultValue": "1995-01-01",
108+
"widgetIds": [
109+
"load_portfolios_custom_obb",
110+
"load_factors_custom_obb",
111+
],
112+
},
113+
{
114+
"name": "Group 4",
115+
"type": "param",
116+
"paramName": "end_date",
117+
"defaultValue": "2025-12-31",
118+
"widgetIds": [
119+
"load_portfolios_custom_obb",
120+
"load_factors_custom_obb",
121+
],
122+
},
123+
],
124+
}
125+
]
126+
127+
128+
@app.get(
129+
"/fama_french_info",
130+
openapi_extra={
131+
"widget_config": {"refetchInterval": False},
132+
"name": "F-F Datasets Info",
133+
},
134+
)
135+
async def get_fama_french_info() -> str:
136+
"""Get Fama French info."""
137+
descriptions = ""
138+
139+
if not DATASETS:
140+
await asyncio.sleep(2)
141+
142+
if DATASETS.get("loaded_portfolio"):
143+
descrip = DATASETS["loaded_portfolio"].get("meta", {}).get("description", "")
144+
descriptions += (
145+
"\n\n"
146+
+ f"### {LOADED_PORTFOLIO.replace('_', ' ')}"
147+
+ "\n\n"
148+
+ descrip
149+
+ "\n\n"
150+
if descrip
151+
else ""
152+
)
153+
if DATASETS.get("loaded_factors"):
154+
descrip = DATASETS["loaded_factors"].get("meta", {}).get("description", "")
155+
descriptions += (
156+
"\n\n"
157+
+ f"### {LOADED_FACTORS.replace('_', ' ')}"
158+
+ "\n\n"
159+
+ descrip
160+
+ "\n\n"
161+
if descrip
162+
else ""
163+
)
164+
165+
return descriptions + "\n\n" + FactorsDoc
166+
167+
168+
@app.get(
169+
"/load_portfolios",
170+
openapi_extra={
171+
"widget_config": {"refecthInterval": False, "name": "F-F Portfolio Data"}
172+
},
173+
)
174+
async def get_load_portfolios(
175+
portfolio: Annotated[
176+
str,
177+
Query(
178+
description="Portfolio set to load.",
179+
json_schema_extra={
180+
"x-widget_config": {
181+
"style": {"popupWidth": 450},
182+
"options": [
183+
d for d in DATASET_CHOICES if "Portfolio" in d.get("value")
184+
],
185+
},
186+
},
187+
),
188+
] = "Portfolios_Formed_on_ME",
189+
measure: Annotated[
190+
Literal["Value", "Equal", "Number Of Firms", "Firm Size"],
191+
Query(description="Data measurement"),
192+
] = "Value",
193+
frequency: Annotated[
194+
Literal["Monthly", "Annual"],
195+
Query(
196+
description="Data frequency. Only valid when the dataset is not daily (in the name)."
197+
),
198+
] = "Monthly",
199+
start_date: Annotated[
200+
Optional[str],
201+
Query(
202+
description="Start date",
203+
json_schema_extra={"x-widget_config": {"value": "$currentDate-30y"}},
204+
),
205+
] = None,
206+
end_date: Annotated[
207+
Optional[str],
208+
Query(
209+
description="End date",
210+
json_schema_extra={"x-widget_config": {"value": "$currentDate"}},
211+
),
212+
] = None,
213+
) -> list:
214+
"""Get dataset."""
215+
global DATASETS, LOADED_PORTFOLIO
216+
217+
if not portfolio:
218+
return "Please select a portfolio."
219+
frequency = None if "Daily" in portfolio else frequency.lower()
220+
measure = measure.replace(" ", "_").lower()
221+
try:
222+
data, meta = get_portfolio_data(portfolio, frequency, measure)
223+
224+
df = data[0]
225+
if start_date:
226+
df = df[df.index >= start_date]
227+
if end_date:
228+
df = df[df.index <= end_date]
229+
DATASETS["loaded_portfolio"] = {"meta": meta[0], "data": df}
230+
LOADED_PORTFOLIO = portfolio
231+
return df.reset_index().to_dict(orient="records") if data else []
232+
except Exception:
233+
return []
234+
235+
236+
@app.get(
237+
"/load_factors",
238+
openapi_extra={
239+
"widget_config": {"refetchInterval": False, "name": "F-F Factors Data"}
240+
},
241+
)
242+
async def get_load_factors(
243+
factor: Annotated[
244+
str,
245+
Query(
246+
description="Factor",
247+
json_schema_extra={
248+
"x-widget_config": {
249+
"style": {"popupWidth": 450},
250+
"options": [
251+
d
252+
for d in DATASET_CHOICES
253+
if "Factors" in d.get("value") or "Factor" in d.get("value")
254+
],
255+
},
256+
},
257+
),
258+
] = "F-F_Research_Data_5_Factors_2x3",
259+
frequency: Annotated[
260+
Literal["Monthly", "Annual"],
261+
Query(
262+
description="Data frequency. Only valid when the dataset is not daily or weekly (in the name)."
263+
),
264+
] = "Monthly",
265+
start_date: Annotated[
266+
Optional[str],
267+
Query(
268+
description="Start date",
269+
json_schema_extra={"x-widget_config": {"value": "$currentDate-30y"}},
270+
),
271+
] = None,
272+
end_date: Annotated[
273+
Optional[str],
274+
Query(
275+
description="End date",
276+
json_schema_extra={"x-widget_config": {"value": "$currentDate"}},
277+
),
278+
] = None,
279+
) -> list:
280+
"""Get dataset."""
281+
global DATASETS, LOADED_FACTORS
282+
283+
if not factor:
284+
return "Please select a factor."
285+
286+
if "daily" in factor or "weekly" in factor:
287+
frequency = None
288+
289+
LOADED_FACTORS = factor
290+
try:
291+
dfs, meta = get_portfolio_data(factor, frequency)
292+
df = dfs[0]
293+
if start_date:
294+
df = df[df.index >= start_date]
295+
if end_date:
296+
df = df[df.index <= end_date]
297+
DATASETS["loaded_factors"] = {"meta": meta[0], "data": df}
298+
return df.reset_index().to_dict(orient="records")
299+
except Exception:
300+
return []
301+
302+
303+
def main():
304+
"""Run the app."""
305+
import subprocess
306+
307+
subprocess.run(
308+
[
309+
"openbb-api",
310+
"--app",
311+
__file__,
312+
"--port",
313+
"6020",
314+
]
315+
)
316+
317+
318+
if __name__ == "__main__":
319+
main()

0 commit comments

Comments
 (0)