Skip to content

Commit 06041a8

Browse files
deeleeramonepiiq
andauthored
[Feature] Add Svensson Nominal Yield Curve From Federal Reserve (#7338)
* add svensson yield curve, cleanup fomc documents endpoint, make provider a standalone extension, move apps from platform-api into extension, and update some models to add prefix/suffix in widget definitions * black * do what codeQL says * black again * use get_args instead of Literal.__args__ * linting * test items * grammar police * test functions need to start with test_* * test params * add total factor productivity from sf fed * openpyxl required for Excel file handling * grammar police * inflation expectations * Update pr-description check action to properly handle quotes --------- Co-authored-by: deeleeramone <> Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
1 parent 4ab9e27 commit 06041a8

39 files changed

+49136
-512
lines changed

.github/workflows/pr_description.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Check PR description
1717
shell: bash
1818
run: |
19-
body="${{ github.event.pull_request.body }}"
19+
body="$(jq -r .pull_request.body "$GITHUB_EVENT_PATH")"
2020
if [ -z "$(echo "$body" | tr -d '[:space:]')" ]; then
2121
echo "::error::PR description is empty. Please provide a description of the change."
2222
exit 1

openbb_platform/core/openbb_core/provider/standard_models/federal_funds_rate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@ class FederalFundsRateData(Data):
6969
json_schema_extra={
7070
"x-unit_measurement": "currency",
7171
"x-frontend_multiply": 1e9,
72+
"x-widget_config": {"prefix": "$", "suffix": "B"},
7273
},
7374
)

openbb_platform/core/openbb_core/provider/standard_models/money_measures.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
DATA_DESCRIPTIONS,
99
QUERY_DESCRIPTIONS,
1010
)
11-
from pydantic import Field
11+
from pydantic import AliasGenerator, ConfigDict, Field
1212

1313

1414
class MoneyMeasuresQueryParams(QueryParams):
@@ -30,22 +30,52 @@ class MoneyMeasuresQueryParams(QueryParams):
3030
class MoneyMeasuresData(Data):
3131
"""Money Measures Data."""
3232

33+
model_config = ConfigDict(
34+
json_schema_extra={
35+
"x-widget_config": {
36+
"$.refetchInterval": False,
37+
}
38+
},
39+
alias_generator=AliasGenerator(
40+
serialization_alias=lambda x: x,
41+
),
42+
)
43+
3344
month: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
34-
M1: float = Field(description="Value of the M1 money supply in billions.")
35-
M2: float = Field(description="Value of the M2 money supply in billions.")
45+
m1: float = Field(
46+
description="Value of the M1 money supply in billions.",
47+
json_schema_extra={
48+
"x-widget_config": {"prefix": "$", "suffix": "B", "headerName": "M1"}
49+
},
50+
)
51+
m2: float = Field(
52+
description="Value of the M2 money supply in billions.",
53+
json_schema_extra={
54+
"x-widget_config": {"prefix": "$", "suffix": "B", "headerName": "M2"}
55+
},
56+
)
3657
currency: float | None = Field(
37-
description="Value of currency in circulation in billions.", default=None
58+
description="Value of currency in circulation in billions.",
59+
default=None,
60+
json_schema_extra={"x-widget_config": {"prefix": "$", "suffix": "B"}},
3861
)
3962
demand_deposits: float | None = Field(
40-
description="Value of demand deposits in billions.", default=None
63+
description="Value of demand deposits in billions.",
64+
default=None,
65+
json_schema_extra={"x-widget_config": {"prefix": "$", "suffix": "B"}},
4166
)
4267
retail_money_market_funds: float | None = Field(
43-
description="Value of retail money market funds in billions.", default=None
68+
description="Value of retail money market funds in billions.",
69+
default=None,
70+
json_schema_extra={"x-widget_config": {"prefix": "$", "suffix": "B"}},
4471
)
4572
other_liquid_deposits: float | None = Field(
46-
description="Value of other liquid deposits in billions.", default=None
73+
description="Value of other liquid deposits in billions.",
74+
default=None,
75+
json_schema_extra={"x-widget_config": {"prefix": "$", "suffix": "B"}},
4776
)
4877
small_denomination_time_deposits: float | None = Field(
4978
description="Value of small denomination time deposits in billions.",
5079
default=None,
80+
json_schema_extra={"x-widget_config": {"prefix": "$", "suffix": "B"}},
5181
)

openbb_platform/core/openbb_core/provider/standard_models/overnight_bank_funding_rate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ class OvernightBankFundingRateData(Data):
5959
json_schema_extra={
6060
"x-unit_measurement": "currency",
6161
"x-frontend_multiply": 1e9,
62+
"x-widget_config": {"prefix": "$", "suffix": "B"},
6263
},
6364
)

openbb_platform/core/openbb_core/provider/standard_models/sofr.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ class SOFRData(Data):
5959
json_schema_extra={
6060
"x-unit_measurement": "currency",
6161
"x-frontend_multiply": 1e9,
62+
"x-widget_config": {"prefix": "$", "suffix": "B"},
6263
},
6364
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Utilities for LRU caching."""
2+
3+
# pylint: disable=W0613
4+
5+
import time
6+
from collections.abc import Callable
7+
from functools import lru_cache, update_wrapper
8+
from math import floor
9+
from typing import Any
10+
11+
12+
def ttl_cache(maxsize: int = 128, typed: bool = False, ttl: int = -1):
13+
"""Cache a function's return value each ttl seconds."""
14+
if ttl <= 0:
15+
ttl = 65536
16+
17+
hash_gen = _ttl_hash_gen(ttl)
18+
19+
def wrapper(func: Callable) -> Callable:
20+
"""Wrap the function for ttl_cache."""
21+
22+
@lru_cache(maxsize, typed)
23+
def ttl_func(ttl_hash, *args, **kwargs):
24+
return func(*args, **kwargs)
25+
26+
def wrapped(*args, **kwargs) -> Any:
27+
"""Wrap the function for ttl_cache."""
28+
th = next(hash_gen)
29+
return ttl_func(th, *args, **kwargs)
30+
31+
return update_wrapper(wrapped, func)
32+
33+
return wrapper
34+
35+
36+
def _ttl_hash_gen(seconds: int):
37+
start_time = time.time()
38+
39+
while True:
40+
yield floor((time.time() - start_time) / seconds)
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""Tests for the LRU cache utilities."""
2+
3+
import time
4+
5+
from openbb_core.provider.utils.lru import _ttl_hash_gen, ttl_cache
6+
7+
8+
class TestTtlHashGen:
9+
"""Tests for _ttl_hash_gen."""
10+
11+
def test_yields_same_value_within_ttl_window(self):
12+
"""Test that the generator yields the same value within a TTL window."""
13+
gen = _ttl_hash_gen(seconds=10)
14+
first = next(gen)
15+
second = next(gen)
16+
assert first == second
17+
18+
def test_yields_different_value_after_ttl_expires(self):
19+
"""Test that the generator yields a different value after TTL expires."""
20+
gen = _ttl_hash_gen(seconds=1)
21+
first = next(gen)
22+
time.sleep(1.1)
23+
second = next(gen)
24+
assert second > first
25+
26+
def test_increments_by_one_per_ttl_period(self):
27+
"""Test that the hash increments by 1 for each TTL period elapsed."""
28+
gen = _ttl_hash_gen(seconds=1)
29+
first = next(gen)
30+
time.sleep(2.1)
31+
second = next(gen)
32+
assert second >= first + 2
33+
34+
35+
class TestTtlCache:
36+
"""Tests for ttl_cache decorator."""
37+
38+
def test_caches_return_value(self):
39+
"""Test that the decorated function's return value is cached."""
40+
call_count = 0
41+
42+
@ttl_cache(ttl=60)
43+
def expensive_function():
44+
nonlocal call_count
45+
call_count += 1
46+
return "result"
47+
48+
result1 = expensive_function()
49+
result2 = expensive_function()
50+
51+
assert result1 == "result"
52+
assert result2 == "result"
53+
assert call_count == 1
54+
55+
def test_cache_expires_after_ttl(self):
56+
"""Test that the cache expires after TTL seconds."""
57+
call_count = 0
58+
59+
@ttl_cache(ttl=1)
60+
def expensive_function():
61+
nonlocal call_count
62+
call_count += 1
63+
return f"result_{call_count}"
64+
65+
result1 = expensive_function()
66+
assert call_count == 1
67+
68+
time.sleep(1.1)
69+
70+
result2 = expensive_function()
71+
assert call_count == 2
72+
assert result1 == "result_1"
73+
assert result2 == "result_2"
74+
75+
def test_caches_with_arguments(self):
76+
"""Test that caching works correctly with function arguments."""
77+
call_count = 0
78+
79+
@ttl_cache(ttl=60)
80+
def func_with_args(x, y):
81+
nonlocal call_count
82+
call_count += 1
83+
return x + y
84+
85+
result1 = func_with_args(1, 2)
86+
result2 = func_with_args(1, 2)
87+
result3 = func_with_args(3, 4)
88+
89+
assert result1 == 3
90+
assert result2 == 3
91+
assert result3 == 7
92+
assert call_count == 2 # Called once for (1,2) and once for (3,4)
93+
94+
def test_caches_with_kwargs(self):
95+
"""Test that caching works correctly with keyword arguments."""
96+
call_count = 0
97+
98+
@ttl_cache(ttl=60)
99+
def func_with_kwargs(a, b=10):
100+
nonlocal call_count
101+
call_count += 1
102+
return a * b
103+
104+
result1 = func_with_kwargs(5, b=2)
105+
result2 = func_with_kwargs(5, b=2)
106+
result3 = func_with_kwargs(5, b=3)
107+
108+
assert result1 == 10
109+
assert result2 == 10
110+
assert result3 == 15
111+
assert call_count == 2
112+
113+
def test_default_ttl_is_large(self):
114+
"""Test that default TTL (when <= 0) is set to a large value."""
115+
call_count = 0
116+
117+
@ttl_cache(ttl=0)
118+
def func():
119+
nonlocal call_count
120+
call_count += 1
121+
return "cached"
122+
123+
func()
124+
func()
125+
func()
126+
127+
assert call_count == 1
128+
129+
def test_negative_ttl_uses_default(self):
130+
"""Test that negative TTL uses the default large value."""
131+
call_count = 0
132+
133+
@ttl_cache(ttl=-1)
134+
def func():
135+
nonlocal call_count
136+
call_count += 1
137+
return "cached"
138+
139+
func()
140+
func()
141+
142+
assert call_count == 1
143+
144+
def test_maxsize_limits_cache(self):
145+
"""Test that maxsize parameter limits the cache size."""
146+
call_count = 0
147+
148+
@ttl_cache(maxsize=2, ttl=60)
149+
def func(x):
150+
nonlocal call_count
151+
call_count += 1
152+
return x * 2
153+
154+
# Fill cache
155+
func(1) # call 1
156+
func(2) # call 2
157+
158+
# These should be cached
159+
func(1)
160+
func(2)
161+
assert call_count == 2
162+
163+
# This evicts the oldest (1)
164+
func(3) # call 3
165+
assert call_count == 3
166+
167+
# 1 was evicted, so this is a new call
168+
func(1) # call 4
169+
assert call_count == 4
170+
171+
def test_typed_parameter(self):
172+
"""Test that typed=True distinguishes between types."""
173+
call_count = 0
174+
175+
@ttl_cache(maxsize=128, typed=True, ttl=60)
176+
def func(x):
177+
nonlocal call_count
178+
call_count += 1
179+
return x
180+
181+
func(1)
182+
func(1.0) # Different type, should be cached separately
183+
184+
assert call_count == 2
185+
186+
def test_preserves_function_metadata(self):
187+
"""Test that the decorator preserves function name and docstring."""
188+
189+
@ttl_cache(ttl=60)
190+
def my_function():
191+
"""My docstring."""
192+
return "result"
193+
194+
assert my_function.__name__ == "my_function"
195+
assert my_function.__doc__ == "My docstring."
196+
197+
def test_works_with_none_return_value(self):
198+
"""Test that None return values are cached correctly."""
199+
call_count = 0
200+
201+
@ttl_cache(ttl=60)
202+
def func_returning_none():
203+
nonlocal call_count
204+
call_count += 1
205+
206+
result1 = func_returning_none()
207+
result2 = func_returning_none()
208+
209+
assert result1 is None
210+
assert result2 is None
211+
assert call_count == 1
212+
213+
def test_works_with_mutable_return_value(self):
214+
"""Test caching with mutable return values (returns same object)."""
215+
call_count = 0
216+
217+
@ttl_cache(ttl=60)
218+
def func_returning_list():
219+
nonlocal call_count
220+
call_count += 1
221+
return [1, 2, 3]
222+
223+
result1 = func_returning_list()
224+
result2 = func_returning_list()
225+
226+
assert result1 == [1, 2, 3]
227+
assert result1 is result2 # Same cached object
228+
assert call_count == 1
229+
230+
def test_concurrent_calls_within_ttl(self):
231+
"""Test that concurrent-like calls within TTL share cache."""
232+
call_count = 0
233+
234+
@ttl_cache(ttl=60)
235+
def func():
236+
nonlocal call_count
237+
call_count += 1
238+
return "result"
239+
240+
# Simulate multiple rapid calls
241+
results = [func() for _ in range(100)]
242+
243+
assert all(r == "result" for r in results)
244+
assert call_count == 1

0 commit comments

Comments
 (0)