diff --git a/README.md b/README.md index 607f0d0..8f3304c 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,12 @@ ARGUS is a Python-based market analytics project evolving from a small FX conver ARGUS is currently focused on building a clean local foundation: -- currency conversion using live ExchangeRate API data -- historical market-data retrieval using yfinance +- currency conversion using live exchange-rate data - calculator and conversion logic - input validation and error handling - Tkinter GUI prototype - legacy CLI/debug interface -- pandas/matplotlib-based analytics prototype +- first pandas/matplotlib-based analytics prototype - tests and documentation > [!IMPORTANT] @@ -67,16 +66,19 @@ Each roadmap phase is treated as a separate development sprint. The roadmap desc ## Current Features - Calculator -- Currency conversion using live ExchangeRate API data -- Historical market-data retrieval using yfinance +- Currency conversion using live exchange rates - Input validation and error handling - Tkinter GUI prototype - Legacy CLI/debug interface - Basic pandas-based trend metrics - Matplotlib-based trend visualization -- Basic client, service and analytics pipeline +- Mock time-series data for early analytics development - Basic test suite +> [!CAUTION] +> Historical market data support is still limited. +> The current live exchange-rate client is useful for simple conversion, but future analytics work will require additional data sources such as Frankfurter or yfinance. + --- ## Project Structure @@ -113,7 +115,6 @@ README.md - requests - python-dotenv -- yfinance - pandas - NumPy - matplotlib @@ -123,7 +124,6 @@ README.md ### Current data source - ExchangeRate API for live currency conversion -- yfinance for historical market-data retrieval and analytics --- @@ -136,6 +136,7 @@ Planned or likely future technologies include: ### Data sources - Frankfurter API for historical FX data +- yfinance for broader market data - possible additional market-data APIs later ### Data processing @@ -192,7 +193,7 @@ Before running ARGUS locally, make sure you have: - Python 3.11 or newer - Git - pip -- an ExchangeRate API key for live currency conversion. Historical analytics currently use yfinance and do not require an additional API key. +- an ExchangeRate API key for live currency conversion Recommended for development: @@ -341,9 +342,9 @@ The project now has a runnable local Python application, a Tkinter GUI prototype Current focus: -- continue Sprint 2 — Market Analytics & Data Source Expansion -- extend historical market-data support beyond the first yfinance client +- start Sprint 2 — Market Analytics & Data Source Expansion +- improve historical exchange-rate data support - add stronger market metrics - expand pandas-based analytics workflows - improve dashboard usefulness without adding unnecessary chart noise -- document metric definitions, assumptions and data-source behavior \ No newline at end of file +- document metric definitions, assumptions and data-source behavior diff --git a/pyproject.toml b/pyproject.toml index e2dc784..665f963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ dependencies = [ "pandas", "numpy", "matplotlib", - "yfinance", ] [project.optional-dependencies] diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index 1293361..d475bc7 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -1,8 +1,9 @@ import matplotlib.pyplot as plt +import pandas as pd from argus.services.timeseries_service import prepare_trend_analysis -def create_trendchart(curr_symbol: str, start: str, end: str, interval: str): +def create_trendchart(curr: str, dates: pd.DataFrame): """ Create a trend chart for exchange-rate analysis. @@ -30,10 +31,8 @@ def create_trendchart(curr_symbol: str, start: str, end: str, interval: str): Minimum and maximum exchange-rate values are marked with scatter points and annotations. """ - result = prepare_trend_analysis(curr_symbol, start, end, interval) - if result is None: - return None - df, min_max_rates = result + df = pd.DataFrame() + df, min_max_rates = prepare_trend_analysis(curr, dates) min_date = min_max_rates["min_date"][0] min_rate = min_max_rates["min_rate"][0] max_date = min_max_rates["max_date"][0] diff --git a/src/legacy/clients/mock_client.py b/src/argus/clients/mock_client.py similarity index 100% rename from src/legacy/clients/mock_client.py rename to src/argus/clients/mock_client.py diff --git a/src/argus/clients/yfinance_client.py b/src/argus/clients/yfinance_client.py deleted file mode 100644 index de86041..0000000 --- a/src/argus/clients/yfinance_client.py +++ /dev/null @@ -1,43 +0,0 @@ -import yfinance as yf -import logging - - -def get_timeseries(curr_symbol, start, end, interval): - """ - Fetch historical exchange-rate time series data from Yahoo Finance. - - Args: - curr_symbol (str): Currency symbol used by Yahoo Finance, for example - "EURUSD=X". - start (str): Start date of the requested time range in YYYY-MM-DD format. - end (str): End date of the requested time range in YYYY-MM-DD format. - interval (str): Data interval supported by Yahoo Finance, for example - "1d", "1h", or "15m". - - Returns: - pandas.DataFrame | None: A DataFrame containing the columns ``date`` and - ``rate`` if data was successfully fetched. Returns ``None`` if the - request fails, returns no data, or an exception occurs. - """ - try: - yf_logger = logging.getLogger("yfinance") - yf_logger.disabled = True - data = yf.download( - tickers=curr_symbol, - start=start, - end=end, - interval=interval, - multi_level_index=False, - progress=False, - ) - yf_logger.disabled = False - if data is None: - return None - if data.empty: - return None - data = data.reset_index() - data = data[["Date", "Close"]] - data = data.rename(columns={"Date": "date", "Close": "rate"}) - return data - except Exception: - return None diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index 9a19523..829a905 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -1,4 +1,5 @@ import tkinter as tk +import pandas as pd from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from argus.analytics.charts.trend_chart import create_trendchart from argus.services.calculator_service import calc, check_op @@ -76,10 +77,27 @@ def show_trend() -> None: global trend_canvas global trend_chart_widget - curr_symbol = "EURUSD=X" - start = "2024-01-01" - end = "2025-01-01" - interval = "1d" + mock_dates = { + "date": [ + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + "2026-06-06", + "2026-06-07", + "2026-06-08", + "2026-06-09", + "2026-06-10", + "2026-06-11", + "2026-06-12", + "2026-06-13", + "2026-06-14", + "2026-06-15", + ] + } + mock_dates = pd.DataFrame(mock_dates) + mock_curr = "USD" calc_frame.pack_forget() conv_frame.pack_forget() @@ -90,16 +108,14 @@ def show_trend() -> None: content.pack(side="top", fill=tk.BOTH, expand=True) if trend_canvas is None: - fig = create_trendchart(curr_symbol, start, end, interval) - if fig is None: - return None + fig = create_trendchart(mock_curr, mock_dates) fig.set_size_inches(7, 4) trend_canvas = FigureCanvasTkAgg(fig, master=content) trend_chart_widget = trend_canvas.get_tk_widget() if trend_chart_widget is None: - return None + return trend_canvas.draw() trend_chart_widget.pack(fill=tk.BOTH, expand=True) diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index b6251bb..3646200 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -1,5 +1,5 @@ import pandas as pd -from argus.clients.yfinance_client import get_timeseries +from argus.clients.mock_client import get_mock_timeseries from argus.analytics.metrics.trend_metrics import ( add_rolling_average, add_daily_percentage_change, @@ -8,33 +8,24 @@ def prepare_trend_analysis( - curr_symbol: str, start: str, end: str, intervall: str -) -> tuple[pd.DataFrame, dict] | None: + mock_curr: str, df: pd.DataFrame +) -> tuple[pd.DataFrame, dict]: """ - Prepare time-series data for trend analysis. + Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. - Fetches historical exchange-rate data for the given currency symbol and - enriches it with daily percentage changes and a rolling average. It also - calculates the minimum and maximum exchange rates for the resulting time - series. + Arg1: mock_curr: str - the currency code for which the trend analysis is to + be performed + Arg2: df: pd.DataFrame - the DataFrame containing the dates for which the + conversion rates are to be added - Args: - curr_symbol (str): Currency symbol used by Yahoo Finance, for example - "EURUSD=X". - start (str): Start date of the requested time range in YYYY-MM-DD format. - end (str): End date of the requested time range in YYYY-MM-DD format. - intervall (str): Data interval supported by Yahoo Finance, for example - "1d", "1h", or "15m". - - Returns: - tuple[pd.DataFrame, dict] | None: A tuple containing the prepared - DataFrame and a dictionary with minimum and maximum rates. Returns - ``None`` if no time-series data could be fetched. + Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, + daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates """ - - df = get_timeseries(curr_symbol, start, end, intervall) - if df is None: - return None + df["rate"] = 0.0 + # For each date one API call to get the rate + for i in range(len(df)): + date = str(df.loc[i, "date"]) + df.loc[i, "rate"] = get_mock_timeseries(mock_curr, date) df = add_daily_percentage_change(df) df = add_rolling_average(df) min_max_rates = get_min_max_rates(df) diff --git a/src/legacy/analytics/__init__.py b/src/legacy/analytics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/legacy/analytics/charts/__init__.py b/src/legacy/analytics/charts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/legacy/analytics/charts/trend_chart.py b/src/legacy/analytics/charts/trend_chart.py deleted file mode 100644 index e18aeb1..0000000 --- a/src/legacy/analytics/charts/trend_chart.py +++ /dev/null @@ -1,72 +0,0 @@ -import matplotlib.pyplot as plt -import pandas as pd -from legacy.services.timeseries_service import prepare_trend_analysis - - -def create_trendchart(curr: str, dates: pd.DataFrame): - """ - Create a trend chart for exchange-rate analysis. - - Builds a Matplotlib figure showing the exchange rate, its rolling - average, and the daily percentage change for a selected currency. - The minimum and maximum exchange-rate values are highlighted in the - chart. - - Args: - curr (str): Currency code or currency pair identifier used for - the trend analysis. - dates (pd.DataFrame): DataFrame containing the date information - used to prepare the time-series analysis. - - Returns: - matplotlib.figure.Figure: Matplotlib figure containing the trend - chart. - - Notes: - The chart uses two y-axes: - - - The left y-axis displays the exchange rate and rolling average. - - The right y-axis displays the daily percentage change. - - Minimum and maximum exchange-rate values are marked with scatter - points and annotations. - """ - df = pd.DataFrame() - df, min_max_rates = prepare_trend_analysis(curr, dates) - min_date = min_max_rates["min_date"][0] - min_rate = min_max_rates["min_rate"][0] - max_date = min_max_rates["max_date"][0] - max_rate = min_max_rates["max_rate"][0] - - # Rate and Rolling Average needs seperat x-Achse von Daily Percentage Chnage erhalten - fig, ax1 = plt.subplots(figsize=(5, 3.5), dpi=100) - - # Subplot 1 - ax1.plot(df["date"], df["rate"], color="black", label="Exchange Rate") - ax1.plot(df["date"], df["roll_avg"], color="blue", label="Rolling Average") - - # Scatter and Annote Min/Max Rate - ax1.scatter(min_date, min_rate, color="red") - ax1.scatter(max_date, max_rate, color="green") - ax1.annotate("Min", (min_date, min_rate)) - ax1.annotate("Max", (max_date, max_rate)) - - # Rotate date values for better visibillity - ax1.tick_params(axis="x", rotation=45) - - # Subplot 2 - ax2 = ax1.twinx() - bar_colors = ["green" if x >= 0 else "red" for x in df["daily_pct_change"]] - ax2.bar( - df["date"], - df["daily_pct_change"], - color=bar_colors, - alpha=0.4, - label="Daily Change", - ) - ax2.legend(loc="upper left") - ax2.set_ylabel("Percentage Scale") - - # Adjust the layout - fig.tight_layout() - return fig diff --git a/src/legacy/analytics/metrics/__init__.py b/src/legacy/analytics/metrics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/legacy/analytics/metrics/trend_metrics.py b/src/legacy/analytics/metrics/trend_metrics.py deleted file mode 100644 index 5622acb..0000000 --- a/src/legacy/analytics/metrics/trend_metrics.py +++ /dev/null @@ -1,75 +0,0 @@ -import pandas as pd - - -def add_daily_percentage_change(df: pd.DataFrame) -> pd.DataFrame: - """ - Add the daily percentage change of the exchange rate. - - Calculates the percentage change between each rate value and the - previous rate value. The result is added as a new column named - ``daily_pct_change``. - - Args: - df (pd.DataFrame): DataFrame containing at least a ``rate`` column. - - Returns: - pd.DataFrame: A copy of the input DataFrame with an added - ``daily_pct_change`` column. - - Notes: - The first row will contain ``NaN`` because there is no previous - rate value to compare against. - """ - result = df.copy() - result["daily_pct_change"] = result["rate"].pct_change() * 100 - return result - - -def add_rolling_average(df: pd.DataFrame) -> pd.DataFrame: - """ - Add a rolling average of the exchange rate. - - Calculates a rolling mean over the ``rate`` column using a fixed - window size of 3 rows. The result is added as a new column named - ``roll_avg``. - - Args: - df (pd.DataFrame): DataFrame containing at least a ``rate`` column. - - Returns: - pd.DataFrame: A copy of the input DataFrame with an added - ``roll_avg`` column. - """ - result = df.copy() - result["roll_avg"] = result["rate"].rolling(window=3, min_periods=1).mean() - return result - - -def get_min_max_rates(df: pd.DataFrame) -> dict: - """ - Get the minimum and maximum exchange-rate values. - - Finds the rows with the lowest and highest values in the ``rate`` - column and returns their dates and rates in a dictionary. - - Args: - df (pd.DataFrame): DataFrame containing at least ``date`` and - ``rate`` columns. - - Returns: - dict: Dictionary containing the minimum and maximum rate data with - the following keys: - - - ``min_date``: Date of the lowest exchange rate. - - ``min_rate``: Lowest exchange-rate value. - - ``max_date``: Date of the highest exchange rate. - - ``max_rate``: Highest exchange-rate value. - """ - min_max = {"min_date": [], "min_rate": [], "max_date": [], "max_rate": []} - min_id = df["rate"].idxmin() - max_id = df["rate"].idxmax() - min_max["min_date"].append(df.loc[min_id, "date"]) - min_max["min_rate"].append(df.loc[min_id, "rate"]) - min_max["max_date"].append(df.loc[max_id, "date"]) - min_max["max_rate"].append(df.loc[max_id, "rate"]) - return min_max diff --git a/src/legacy/clients/__init__.py b/src/legacy/clients/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/legacy/gui/app.py b/src/legacy/gui/app.py deleted file mode 100644 index ff9d790..0000000 --- a/src/legacy/gui/app.py +++ /dev/null @@ -1,126 +0,0 @@ -import tkinter as tk -import pandas as pd -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from legacy.analytics.charts.trend_chart import create_trendchart - - -def on_close() -> None: - """ - Handles the closing of the application window. It ensures that any open trend chart is destroyed and - the application is properly closed. - """ - if trend_chart_widget is not None: - trend_chart_widget.destroy() - - root.quit() - root.destroy() - - -def hide_trend_chart() -> None: - """ - Hides the trend chart from the GUI if it is currently displayed. - """ - if trend_chart_widget is not None: - trend_chart_widget.pack_forget() - - -def show_menu() -> None: - """ - Displays the main menu in the application. It updates the GUI to show the menu interface. - """ - app_frame.pack_forget() - hide_trend_chart() - - menu_frame.pack(side="right", fill=tk.BOTH, expand=True) - - -def show_trend() -> None: - """ - Displays the trend chart in the application. It prepares the data for trend analysis, - creates the trend chart, and updates the GUI to show the chart. - """ - global trend_canvas - global trend_chart_widget - - mock_dates = { - "date": [ - "2026-06-01", - "2026-06-02", - "2026-06-03", - "2026-06-04", - "2026-06-05", - "2026-06-06", - "2026-06-07", - "2026-06-08", - "2026-06-09", - "2026-06-10", - "2026-06-11", - "2026-06-12", - "2026-06-13", - "2026-06-14", - "2026-06-15", - ] - } - mock_dates = pd.DataFrame(mock_dates) - mock_curr = "USD" - - menu_frame.pack_forget() - - app_frame.pack(fill=tk.BOTH, expand=True) - sidebar.pack(side="top", fill="x") - content.pack(side="top", fill=tk.BOTH, expand=True) - - if trend_canvas is None: - fig = create_trendchart(mock_curr, mock_dates) - fig.set_size_inches(7, 4) - - trend_canvas = FigureCanvasTkAgg(fig, master=content) - trend_chart_widget = trend_canvas.get_tk_widget() - - if trend_chart_widget is None: - return - trend_canvas.draw() - trend_chart_widget.pack(fill=tk.BOTH, expand=True) - - -def app() -> None: - """ - The main function that initializes and starts the GUI application. It sets up the main window, frames, labels, entries, and buttons, and defines the layout of the application. - """ - root.mainloop() - - -# Window -root = tk.Tk() -root.title("FX-Converter Lab") -root.geometry("800x600") # Width x Length -root.protocol("WM_DELETE_WINDOW", on_close) - -# Frames -menu_frame = tk.Frame(root) -app_frame = tk.Frame(root) -sidebar = tk.Frame(app_frame) -content = tk.Frame(app_frame) - -# Trend chart is loaded lazily -trend_canvas = None -trend_chart_widget = None - -# Labels -menu_label = tk.Label(menu_frame, text="Menu", font=("Arial", 20)) -menu_label.pack(pady=20) - - -# Buttons -from_menu_trend_chart = tk.Button(menu_frame, text="Trend Chart", command=show_trend) -from_sidebar_trend_chart = tk.Button(sidebar, text="Trend Chart", command=show_trend) -return_menu = tk.Button(sidebar, text="Back to menu", command=show_menu) - -from_menu_trend_chart.pack(fill="x", padx=50, pady=15) -from_sidebar_trend_chart.pack(side="left") -return_menu.pack(side="left") - -show_menu() - -if __name__ == "__main__": - app() diff --git a/src/legacy/services/timeseries_service.py b/src/legacy/services/timeseries_service.py deleted file mode 100644 index d79d599..0000000 --- a/src/legacy/services/timeseries_service.py +++ /dev/null @@ -1,32 +0,0 @@ -import pandas as pd -from legacy.clients.mock_client import get_mock_timeseries -from legacy.analytics.metrics.trend_metrics import ( - add_rolling_average, - add_daily_percentage_change, - get_min_max_rates, -) - - -def prepare_trend_analysis( - mock_curr: str, df: pd.DataFrame -) -> tuple[pd.DataFrame, dict]: - """ - Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. - - Arg1: mock_curr: str - the currency code for which the trend analysis is to - be performed - Arg2: df: pd.DataFrame - the DataFrame containing the dates for which the - conversion rates are to be added - - Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, - daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates - """ - df["rate"] = 0.0 - # For each date one API call to get the rate - for i in range(len(df)): - date = str(df.loc[i, "date"]) - df.loc[i, "rate"] = get_mock_timeseries(mock_curr, date) - df = add_daily_percentage_change(df) - df = add_rolling_average(df) - min_max_rates = get_min_max_rates(df) - return df, min_max_rates diff --git a/tests/test_timeseries_service.py b/tests/test_timeseries_service.py index 7dd3c9f..dbc5675 100644 --- a/tests/test_timeseries_service.py +++ b/tests/test_timeseries_service.py @@ -4,31 +4,26 @@ from argus.services.timeseries_service import prepare_trend_analysis -def test_get_a_full_timeseries(): - test_curr = "EURUSD=X" - test_start = "2024-01-01" - test_end = "2024-01-04" - test_interval = "1d" +def test_is_pct_change_added(): + test_curr = "USD" + test_dates = { + "date": ["2026-06-01", "2026-06-02", "2026-06-03"], + } + test_dates = pd.DataFrame(test_dates) expect_result = { - "date": ["2024-01-01", "2024-01-02", "2024-01-03"], - "rate": [1.1055831909179688, 1.1038745641708374, 1.0941756963729858], - "daily_pct_change": [np.nan, -0.1545452898675692, -0.8786204622023064], - "roll_avg": [1.1055831909179688, 1.104728877544403, 1.101211150487264], + "date": ["2026-06-01", "2026-06-02", "2026-06-03"], + "rate": [1.08, 1.1, 1.14], + "daily_pct_change": [np.nan, 1.85185185185186, 3.6363636363636154], + "roll_avg": [1.08, 1.09, 1.1066666666666667], } expect_dict = { - "min_date": ["2024-01-03 00:00:00"], - "min_rate": [1.0941756963729858], - "max_date": ["2024-01-01 00:00:00"], - "max_rate": [1.1055831909179688], + "min_date": ["2026-06-01"], + "min_rate": [1.08], + "max_date": ["2026-06-03"], + "max_rate": [1.14], } - result = prepare_trend_analysis(test_curr, test_start, test_end, test_interval) - if result is None: - return False - result_df, result_dict = result - result_df["date"] = result_df["date"].astype("str") - result_dict["min_date"] = [str(result_dict["min_date"][0])] - result_dict["max_date"] = [str(result_dict["max_date"][0])] + result_df, result_dict = prepare_trend_analysis(test_curr, test_dates) expect_df = pd.DataFrame(expect_result) pdt.assert_frame_equal(result_df, expect_df) diff --git a/tests/test_yfinance_client.py b/tests/test_yfinance_client.py deleted file mode 100644 index faf15fc..0000000 --- a/tests/test_yfinance_client.py +++ /dev/null @@ -1,80 +0,0 @@ -from argus.clients.yfinance_client import get_timeseries -import pandas as pd -import pandas.testing as pdt - - -def test_get_dataframe(monkeypatch): - test_resp = pd.DataFrame( - { - "Close": [1.105583, 1.103875, 1.094176], - }, - index=pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), - ) - test_resp.index.name = "Date" - test_curr = "EURUSD=X" - test_start = "2024-01-01" - test_end = "2024-01-04" - test_interval = "1d" - - def fake_yfinance_download(*args, **kwargs): - return test_resp - - monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr, test_start, test_end, test_interval) - expected = pd.DataFrame( - { - "date": pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), - "rate": [1.105583, 1.103875, 1.094176], - } - ) - assert result is not None - pdt.assert_frame_equal(result, expected) - - -def test_get_none(monkeypatch): - test_curr = "EURUSD=X" - test_start = "2024-01-01" - test_end = "2024-01-04" - test_interval = "1d" - - def fake_yfinance_download(*args, **kwargs): - return None - - monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr, test_start, test_end, test_interval) - assert result is None - - -def test_get_empty_frame(monkeypatch): - test_curr = "EURUSD=X" - test_start = "2024-01-01" - test_end = "2024-01-01" - test_interval = "1d" - - def fake_yfinance_download(*args, **kwargs): - return pd.DataFrame() - - monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr, test_start, test_end, test_interval) - assert result is None - - -def test_error_raise(monkeypatch): - test_curr = "EURUSD=X" - # start date is inclusiv and end date is exclusiv - the range 2024-01-01-2024-01-01 is not possible - test_start = "2024-01-04" - test_end = "2024-01-02" - test_interval = "1d" - - def fake_yfinance_download( - tickers=test_curr, start=test_start, end=test_end, interval=test_interval - ): - return Exception("fake yfinance error") - - monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr, test_start, test_end, test_interval) - assert result is None