diff options
author | James Maslek <jmaslek11@gmail.com> | 2024-02-21 12:09:57 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-21 17:09:57 +0000 |
commit | 3cc6025ab4c7cf4195a04e8ad6b2fa2d03abe97d (patch) | |
tree | b7c384bf06677ecf76be956c4de361c83bc6b715 | |
parent | 754e14ca71a8d08ac002036b0fc9edfa1459e44a (diff) |
Update the quantitative extension to make more sense (#6087)
* Split out a rolling submenu for the rolling functions
* Make a performance and a stats submenu.
* Test the statistics functions
* lint
* lint
* dupe test
* pylint
* ruff
* Try tests quick
* black magic signature funcs
* fix my custom tests
* Fix the existing imports/urls
* push the api update
* okay I figured out whats going on
* this should be all of them
* Correct docstringing examples
---------
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
10 files changed, 1298 insertions, 378 deletions
diff --git a/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py b/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py index 6bcaa5da5e0..10e9432d1f8 100644 --- a/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py +++ b/openbb_platform/extensions/quantitative/integration/test_quantitative_api.py @@ -127,12 +127,12 @@ def test_quantitative_capm(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_omega_ratio(params, data_type): +def test_quantitative_performance_omega_ratio(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/omega_ratio?{query_str}" + url = f"http://0.0.0.0:8000/api/v1/quantitative/performance/omega_ratio?{query_str}" result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -146,12 +146,12 @@ def test_quantitative_omega_ratio(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_kurtosis(params, data_type): +def test_quantitative_rolling_kurtosis(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/kurtosis?{query_str}" + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/kurtosis?{query_str}" result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -212,12 +212,14 @@ def test_quantitative_unitroot_test(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_sharpe_ratio(params, data_type): +def test_quantitative_performance_sharpe_ratio(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/sharpe_ratio?{query_str}" + url = ( + f"http://0.0.0.0:8000/api/v1/quantitative/performance/sharpe_ratio?{query_str}" + ) result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -251,12 +253,14 @@ def test_quantitative_sharpe_ratio(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_sortino_ratio(params, data_type): +def test_quantitative_performance_sortino_ratio(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/sortino_ratio?{query_str}" + url = ( + f"http://0.0.0.0:8000/api/v1/quantitative/performance/sortino_ratio?{query_str}" + ) result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -269,12 +273,66 @@ def test_quantitative_sortino_ratio(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_skewness(params, data_type): +def test_quantitative_rolling_skew(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/skew?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_variance(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/variance?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_stdev(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/stdev?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_mean(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/skewness?{query_str}" + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/mean?{query_str}" result = requests.post(url, headers=get_headers(), timeout=60, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -306,12 +364,12 @@ def test_quantitative_skewness(params, data_type): ], ) @pytest.mark.integration -def test_quantitative_quantile(params, data_type): +def test_quantitative_rolling_quantile(params, data_type): params = {p: v for p, v in params.items() if v} data = json.dumps(get_data(data_type)) query_str = get_querystring(params, []) - url = f"http://0.0.0.0:8000/api/v1/quantitative/quantile?{query_str}" + url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/quantile?{query_str}" result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -334,3 +392,133 @@ def test_quantitative_summary(params, data_type): result = requests.post(url, headers=get_headers(), timeout=10, data=data) assert isinstance(result, requests.Response) assert result.status_code == 200 + + +############ +# quantitative/stats +############ + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_skew(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/skew?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_kurtosis(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/kurtosis?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_mean(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/mean?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_stdev(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/stdev?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close", "index": "date"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_variance(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/variance?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=60, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + +@parametrize( + "params, data_type", + [ + ( + { + "data": "", + "target": "close", + "quantile_pct": "", + "index": "date", + }, + "equity", + ), + ( + { + "data": "", + "target": "high", + "quantile_pct": "0.6", + "index": "date", + }, + "crypto", + ), + ], +) +@pytest.mark.integration +def test_quantitative_stats_quantile(params, data_type): + params = {p: v for p, v in params.items() if v} + data = json.dumps(get_data(data_type)) + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/quantile?{query_str}" + result = requests.post(url, headers=get_headers(), timeout=10, data=data) + assert isinstance(result, requests.Response) + assert result.status_code == 200 diff --git a/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py b/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py index f41f26613b1..5c55621a8ae 100644 --- a/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py +++ b/openbb_platform/extensions/quantitative/integration/test_quantitative_python.py @@ -117,11 +117,11 @@ def test_quantitative_capm(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_omega_ratio(params, data_type, obb): +def test_quantitative_performance_omega_ratio(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.omega_ratio(**params) + result = obb.quantitative.performance.omega_ratio(**params) assert result assert isinstance(result, OBBject) @@ -134,11 +134,11 @@ def test_quantitative_omega_ratio(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_kurtosis(params, data_type, obb): +def test_quantitative_rolling_kurtosis(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.kurtosis(**params) + result = obb.quantitative.rolling.kurtosis(**params) assert result assert isinstance(result, OBBject) assert len(result.results) > 0 @@ -203,11 +203,11 @@ def test_quantitative_unitroot_test(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_sharpe_ratio(params, data_type, obb): +def test_quantitative_performance_sharpe_ratio(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.sharpe_ratio(**params) + result = obb.quantitative.performance.sharpe_ratio(**params) assert result assert isinstance(result, OBBject) @@ -240,11 +240,11 @@ def test_quantitative_sharpe_ratio(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_sortino_ratio(params, data_type, obb): +def test_quantitative_performance_sortino_ratio(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.sortino_ratio(**params) + result = obb.quantitative.performance.sortino_ratio(**params) assert result assert isinstance(result, OBBject) @@ -256,11 +256,11 @@ def test_quantitative_sortino_ratio(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_skewness(params, data_type, obb): +def test_quantitative_rolling_skew(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.skewness(**params) + result = obb.quantitative.rolling.skew(**params) assert result assert isinstance(result, OBBject) assert len(result.results) > 0 @@ -292,11 +292,11 @@ def test_quantitative_skewness(params, data_type, obb): ], ) @pytest.mark.integration -def test_quantitative_quantile(params, data_type, obb): +def test_quantitative_rolling_quantile(params, data_type, obb): params = {p: v for p, v in params.items() if v} params["data"] = get_data(data_type) - result = obb.quantitative.quantile(**params) + result = obb.quantitative.rolling.quantile(**params) assert result assert isinstance(result, OBBject) assert len(result.results) > 0 @@ -317,3 +317,228 @@ def test_quantitative_summary(params, data_type, obb): result = obb.quantitative.summary(**params) assert result assert isinstance(result, OBBject) + + +@parametrize( + "params, data_type", + [ + ( + { + "data": "", + "target": "close", + "window": "10", + "quantile_pct": "", + "index": "date", + }, + "equity", + ), + ( + { + "data": "", + "target": "high", + "window": "50", + "quantile_pct": "0.6", + "index": "date", + }, + "crypto", + ), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_stdev(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.rolling.stdev(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ( + { + "data": "", + "target": "close", + "window": "10", + "quantile_pct": "", + "index": "date", + }, + "equity", + ), + ( + { + "data": "", + "target": "high", + "window": "50", + "quantile_pct": "0.6", + "index": "date", + }, + "crypto", + ), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_mean(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.rolling.mean(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ( + { + "data": "", + "target": "close", + "window": "10", + "quantile_pct": "", + "index": "date", + }, + "equity", + ), + ( + { + "data": "", + "target": "high", + "window": "50", + "quantile_pct": "0.6", + "index": "date", + }, + "crypto", + ), + ], +) +@pytest.mark.integration +def test_quantitative_rolling_variance(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.rolling.variance(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_skew(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.skew(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_kurtosis(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.kurtosis(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_variance(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.variance(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_stdev(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.stdev(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ({"data": "", "target": "close"}, "equity"), + ], +) +@pytest.mark.integration +def test_quantitative_stats_mean(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.mean(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + + +@parametrize( + "params, data_type", + [ + ( + { + "data": "", + "target": "close", + "quantile_pct": "", + }, + "equity", + ), + ( + { + "data": "", + "target": "close", + "quantile_pct": "0.6", + }, + "crypto", + ), + ], +) +@pytest.mark.integration +def test_quantitative_stats_quantile(params, data_type, obb): + params = {p: v for p, v in params.items() if v} + params["data"] = get_data(data_type) + + result = obb.quantitative.stats.quantile(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 diff --git a/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py b/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py new file mode 100644 index 00000000000..92bd611a380 --- /dev/null +++ b/openbb_platform/extensions/quantitative/openbb_quantitative/performance/performance_router.py @@ -0,0 +1,203 @@ +from typing import List + +import numpy as np +import pandas as pd +from openbb_core.app.model.obbject import OBBject +from openbb_core.app.router import Router +from openbb_core.app.utils import ( + basemodel_to_df, + df_to_basemodel, + get_target_column, +) +from openbb_core.provider.abstract.data import Data +from openbb_quantitative.helpers import validate_window +from openbb_quantitative.models import ( + OmegaModel, +) +from pydantic import PositiveInt + +router = Router(prefix="/performance") + + +@router.command( + methods=["POST"], + examples=[ + 'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()', + 'returns = stock_data["close"].pct_change().dropna()', + 'obb.quantitative.omega_ratio(data=returns, target="close")', + ], +) +def omega_ratio( + data: List[Data], + target: str, + threshold_start: float = 0.0, + threshold_end: float = 1.5, +) -> OBBject[List[OmegaModel]]: + """Calculate the Omega Ratio. + + The Omega Ratio is a sophisticated metric that goes beyond traditional performance measures by considering the + probability of achieving returns above a given threshold. It offers a more nuanced view of risk and reward, + focusing on the likelihood of success rather than just average outcomes. + + Parameters + ---------- + data : List[Data] + Time series data. + target : str + Target column name. + threshold_start : float, optional + Start threshold, by default 0.0 + threshold_end : float, optional + End threshold, by default 1.5 + + Returns + ------- + OBBject[List[OmegaModel]] + Omega ratios. + """ + df = basemodel_to_df(data) + series_target = get_target_column(df, target) + + epsilon = 1e-6 # to avoid division by zero + + def get_omega_ratio(df_target: pd.Series, threshold: float) -> float: + """Get omega ratio.""" + daily_threshold = (threshold + 1) ** np.sqrt(1 / 252) - 1 + excess = df_target - daily_threshold + numerator = excess[excess > 0].sum() + denominator = -excess[excess < 0].sum() + epsilon + + return numerator / denominator + + threshold = np.linspace(threshold_start, threshold_end, 50) + results = [] + for i in threshold: + omega_ = get_omega_ratio(series_target, i) + results.append(OmegaModel(threshold=i, omega=omega_)) + + return OBBject(results=results) + + +@router.command( + methods=["POST"], + examples=[ + 'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()', + 'returns = stock_data["close"].pct_change().dropna()', + 'obb.quantitative.sharpe_ratio(data=returns, target="close")', + ], +) +def sharpe_ratio( + data: List[Data], + target: str, + rfr: float = 0.0, + window: PositiveInt = 252, + index: str = "date", +) -> OBBject[List[Data]]: + """Get Rolling Sharpe Ratio. + + This function calculates the Sharpe Ratio, a metric used to assess the return of an investment compared to its risk. + By factoring in the risk-free rate, it helps you understand how much extra return you're getting for the extra + volatility that you endure by holding a riskier asset. The Sharpe Ratio is essential for investors looking to + compare the efficiency of different investments, providing a clear picture of potential rewards in relation to their + risks over a specified period. Ideal for gauging the effectiveness of investment strategies, it offers insights into + optimizing your portfolio for maximum return on risk. + + Parameters + ---------- + data : List[Data] + Time series data. + target : str + Target column name. + rfr : float, optional + Risk-free rate, by default 0.0 + window : PositiveInt, optional + Window size, by default 252 + index : str, optional + + Returns + ------- + OBBject[List[Data]] + Sharpe ratio. + """ + df = basemodel_to_df(data, index=index) + series_target = get_target_column(df, target) + validate_window(series_target, window) + series_target.name = f"sharpe_{window}" + returns = series_target.pct_change().dropna().rolling(window).sum() + std = series_target.rolling(window).std() / np.sqrt(window) + results = ((returns - rfr) / std).dropna().reset_index(drop=False) + + results = df_to_basemodel(results) + + return OBBject(results=results) + + +@router.command( + methods=["POST"], + examples=[ + 'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()', + 'returns = stock_data["close"].pct_change().dropna()', + 'obb.quantitative.sortino_ratio(data=stock_data, target="close")', + 'obb |